diff --git a/README.md b/README.md index 91390de..64a7e4d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # atomicmemory-docs -Documentation site for [AtomicMemory](https://github.com/atomicmemory) — the +Documentation site for [AtomicMemory](https://github.com/atomicmemory), the standardized platform layer for AI memory. Live site: **[docs.atomicmemory.ai](https://docs.atomicmemory.ai)** @@ -13,7 +13,7 @@ Built with [Docusaurus](https://docusaurus.io/). Deployed to GitHub Pages. docs/ ├─ introduction.md # Landing page (served at /) ├─ quickstart.md # Docker → first ingest/search in 2 minutes -├─ platform/ # Why modular — the differentiator narrative +├─ platform/ # Why modular, the differentiator narrative │ ├─ architecture.md # Ingest / Search / CRUD / Lifecycle / Trust separation │ ├─ composition.md # createCoreRuntime, createApp, bindEphemeral seams │ ├─ stores.md # MemoryStore / SearchStore / ClaimStore / EntityStore / EpisodeStore @@ -54,7 +54,7 @@ builds always render from the committed vendored spec. ## Contributing `platform/` and `sdk/` pages are authored by hand. `api-reference/http/` -pages are generated — edit `atomicmemory-core/src/schemas/*.ts` and +pages are generated, edit `atomicmemory-core/src/schemas/*.ts` and regenerate via the workflow above to change them. Apache-2.0 licensed. See [atomicmemory-core](https://github.com/atomicmemory/atomicmemory-core) diff --git a/docs/api-reference/http/consolidate-memories.StatusCodes.json b/docs/api-reference/http/consolidate-memories.StatusCodes.json index 5bb40ce..04792aa 100644 --- a/docs/api-reference/http/consolidate-memories.StatusCodes.json +++ b/docs/api-reference/http/consolidate-memories.StatusCodes.json @@ -1 +1 @@ -{"responses":{"200":{"content":{"application/json":{"schema":{"anyOf":[{"description":"Consolidation dry-run (execute=false).","properties":{"clusters":{"items":{"properties":{"avg_affinity":{"type":"number"},"member_contents":{"items":{"type":"string"},"type":"array"},"member_count":{"type":"number"},"member_ids":{"items":{"type":"string"},"type":"array"}},"required":["member_ids","member_contents","avg_affinity","member_count"],"type":"object"},"type":"array"},"clusters_found":{"type":"number"},"memories_in_clusters":{"type":"number"},"memories_scanned":{"type":"number"}},"required":["memories_scanned","clusters_found","memories_in_clusters","clusters"],"type":"object"},{"description":"Consolidation execution result (execute=true).","properties":{"clusters_consolidated":{"type":"number"},"consolidated_memory_ids":{"items":{"type":"string"},"type":"array"},"memories_archived":{"type":"number"},"memories_created":{"type":"number"}},"required":["clusters_consolidated","memories_archived","memories_created","consolidated_memory_ids"],"type":"object"}],"description":"Consolidation result — scan or execute."}}},"description":"Consolidation result."},"400":{"content":{"application/json":{"schema":{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Input validation error"},"500":{"content":{"application/json":{"schema":{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Internal server error"}}} \ No newline at end of file +{"responses":{"200":{"content":{"application/json":{"schema":{"anyOf":[{"description":"Consolidation dry-run (execute=false).","properties":{"clusters":{"items":{"properties":{"avg_affinity":{"type":"number"},"member_contents":{"items":{"type":"string"},"type":"array"},"member_count":{"type":"number"},"member_ids":{"items":{"type":"string"},"type":"array"}},"required":["member_ids","member_contents","avg_affinity","member_count"],"type":"object"},"type":"array"},"clusters_found":{"type":"number"},"memories_in_clusters":{"type":"number"},"memories_scanned":{"type":"number"}},"required":["memories_scanned","clusters_found","memories_in_clusters","clusters"],"type":"object"},{"description":"Consolidation execution result (execute=true).","properties":{"clusters_consolidated":{"type":"number"},"consolidated_memory_ids":{"items":{"type":"string"},"type":"array"},"memories_archived":{"type":"number"},"memories_created":{"type":"number"}},"required":["clusters_consolidated","memories_archived","memories_created","consolidated_memory_ids"],"type":"object"}],"description":"Consolidation result, scan or execute."}}},"description":"Consolidation result."},"400":{"content":{"application/json":{"schema":{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Input validation error"},"500":{"content":{"application/json":{"schema":{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Internal server error"}}} \ No newline at end of file diff --git a/docs/api-reference/http/conventions.md b/docs/api-reference/http/conventions.md index fa60be8..0586e3c 100644 --- a/docs/api-reference/http/conventions.md +++ b/docs/api-reference/http/conventions.md @@ -22,13 +22,13 @@ The canonical error envelope is a single string: This shape applies to 400 (input validation), 404 (resource not found), and 500 (internal server error) on every route. -**Exception — `PUT /v1/memories/config`** ships two richer envelopes so operators know exactly which guard rejected the request: +**Exception, `PUT /v1/memories/config`** ships two richer envelopes so operators know exactly which guard rejected the request: ```json // 400 when a body includes startup-only fields { "error": "Provider/model selection is startup-only", - "detail": "Fields embedding_provider cannot be mutated at runtime — the embedding/LLM provider caches are fixed at first use.", + "detail": "Fields embedding_provider cannot be mutated at runtime, the embedding/LLM provider caches are fixed at first use.", "rejected": ["embedding_provider"] } ``` @@ -45,7 +45,7 @@ This shape applies to 400 (input validation), 404 (resource not found), and 500 |--------|---------| | 400 | Invalid input (missing/malformed fields). Basic `{ error }` on every route except `PUT /v1/memories/config`, which may return the richer envelope above. | | 404 | Resource not found (e.g. `GET /v1/memories/:id` with an unknown id). | -| 410 | Gone — `PUT /v1/memories/config` only, when runtime config mutation is disabled. Uses the richer envelope above. | +| 410 | Gone, `PUT /v1/memories/config` only, when runtime config mutation is disabled. Uses the richer envelope above. | | 500 | Internal server error. | ## CORS diff --git a/docs/api-reference/http/ingest-memory-quick.RequestSchema.json b/docs/api-reference/http/ingest-memory-quick.RequestSchema.json index 8424bb0..d48efe3 100644 --- a/docs/api-reference/http/ingest-memory-quick.RequestSchema.json +++ b/docs/api-reference/http/ingest-memory-quick.RequestSchema.json @@ -1 +1 @@ -{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Ingest a conversation transcript. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation.","type":"object"},"conversation":{"description":"Required. conversation.","minLength":1,"type":"string"},"skip_extraction":{"type":"boolean"},"source_site":{"description":"Required. source_site.","minLength":1,"type":"string"},"source_url":{"type":"string"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","conversation","source_site"],"type":"object"}}},"required":true}} \ No newline at end of file +{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Ingest a conversation transcript. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation.","type":"object"},"conversation":{"description":"Required. conversation.","minLength":1,"type":"string"},"skip_extraction":{"type":"boolean"},"source_site":{"description":"Required. source_site.","minLength":1,"type":"string"},"source_url":{"type":"string"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","conversation","source_site"],"type":"object"}}},"required":true}} \ No newline at end of file diff --git a/docs/api-reference/http/ingest-memory.RequestSchema.json b/docs/api-reference/http/ingest-memory.RequestSchema.json index 8424bb0..d48efe3 100644 --- a/docs/api-reference/http/ingest-memory.RequestSchema.json +++ b/docs/api-reference/http/ingest-memory.RequestSchema.json @@ -1 +1 @@ -{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Ingest a conversation transcript. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation.","type":"object"},"conversation":{"description":"Required. conversation.","minLength":1,"type":"string"},"skip_extraction":{"type":"boolean"},"source_site":{"description":"Required. source_site.","minLength":1,"type":"string"},"source_url":{"type":"string"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","conversation","source_site"],"type":"object"}}},"required":true}} \ No newline at end of file +{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Ingest a conversation transcript. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation.","type":"object"},"conversation":{"description":"Required. conversation.","minLength":1,"type":"string"},"skip_extraction":{"type":"boolean"},"source_site":{"description":"Required. source_site.","minLength":1,"type":"string"},"source_url":{"type":"string"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","conversation","source_site"],"type":"object"}}},"required":true}} \ No newline at end of file diff --git a/docs/api-reference/http/search-memories-fast.RequestSchema.json b/docs/api-reference/http/search-memories-fast.RequestSchema.json index bcedaff..baae367 100644 --- a/docs/api-reference/http/search-memories-fast.RequestSchema.json +++ b/docs/api-reference/http/search-memories-fast.RequestSchema.json @@ -1 +1 @@ -{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Search memories. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"agent_scope":{"description":"Agent-scope filter for workspace searches. String literal 'all' | 'self' | 'others' or a concrete agent_id. Array of agent_ids is also accepted. Any other value is silently ignored.","example":"all","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"as_of":{"description":"ISO-8601 timestamp accepted by temporal search (as_of). Empty string or null means absent; any other non-ISO value is rejected with 400.","example":"2026-01-15T12:00:00Z","format":"date-time","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation.","type":"object"},"limit":{"maximum":100,"minimum":1,"type":"integer"},"namespace_scope":{"type":"string"},"query":{"description":"Required. query.","minLength":1,"type":"string"},"retrieval_mode":{"enum":["flat","tiered","abstract-aware"],"type":"string"},"skip_repair":{"type":"boolean"},"source_site":{"type":"string"},"token_budget":{"maximum":50000,"minimum":100,"type":"integer"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","query"],"type":"object"}}},"required":true}} \ No newline at end of file +{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Search memories. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"agent_scope":{"description":"Agent-scope filter for workspace searches. String literal 'all' | 'self' | 'others' or a concrete agent_id. Array of agent_ids is also accepted. Any other value is silently ignored.","example":"all","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"as_of":{"description":"ISO-8601 timestamp accepted by temporal search (as_of). Empty string or null means absent; any other non-ISO value is rejected with 400.","example":"2026-01-15T12:00:00Z","format":"date-time","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation.","type":"object"},"limit":{"maximum":100,"minimum":1,"type":"integer"},"namespace_scope":{"type":"string"},"query":{"description":"Required. query.","minLength":1,"type":"string"},"retrieval_mode":{"enum":["flat","tiered","abstract-aware"],"type":"string"},"skip_repair":{"type":"boolean"},"source_site":{"type":"string"},"token_budget":{"maximum":50000,"minimum":100,"type":"integer"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","query"],"type":"object"}}},"required":true}} \ No newline at end of file diff --git a/docs/api-reference/http/search-memories.RequestSchema.json b/docs/api-reference/http/search-memories.RequestSchema.json index bcedaff..baae367 100644 --- a/docs/api-reference/http/search-memories.RequestSchema.json +++ b/docs/api-reference/http/search-memories.RequestSchema.json @@ -1 +1 @@ -{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Search memories. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"agent_scope":{"description":"Agent-scope filter for workspace searches. String literal 'all' | 'self' | 'others' or a concrete agent_id. Array of agent_ids is also accepted. Any other value is silently ignored.","example":"all","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"as_of":{"description":"ISO-8601 timestamp accepted by temporal search (as_of). Empty string or null means absent; any other non-ISO value is rejected with 400.","example":"2026-01-15T12:00:00Z","format":"date-time","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation.","type":"object"},"limit":{"maximum":100,"minimum":1,"type":"integer"},"namespace_scope":{"type":"string"},"query":{"description":"Required. query.","minLength":1,"type":"string"},"retrieval_mode":{"enum":["flat","tiered","abstract-aware"],"type":"string"},"skip_repair":{"type":"boolean"},"source_site":{"type":"string"},"token_budget":{"maximum":50000,"minimum":100,"type":"integer"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","query"],"type":"object"}}},"required":true}} \ No newline at end of file +{"title":"Body","body":{"content":{"application/json":{"schema":{"description":"Search memories. User-scoped unless workspace_id + agent_id are both provided.","properties":{"agent_id":{"description":"Optional agent identifier. Silently dropped if empty / non-string.","type":"string"},"agent_scope":{"description":"Agent-scope filter for workspace searches. String literal 'all' | 'self' | 'others' or a concrete agent_id. Array of agent_ids is also accepted. Any other value is silently ignored.","example":"all","oneOf":[{"type":"string"},{"items":{"type":"string"},"type":"array"}]},"as_of":{"description":"ISO-8601 timestamp accepted by temporal search (as_of). Empty string or null means absent; any other non-ISO value is rejected with 400.","example":"2026-01-15T12:00:00Z","format":"date-time","type":"string"},"config_override":{"additionalProperties":{"anyOf":[{"type":"boolean"},{"type":"number"},{"type":"string"},{"type":"null"}]},"description":"Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation.","type":"object"},"limit":{"maximum":100,"minimum":1,"type":"integer"},"namespace_scope":{"type":"string"},"query":{"description":"Required. query.","minLength":1,"type":"string"},"retrieval_mode":{"enum":["flat","tiered","abstract-aware"],"type":"string"},"skip_repair":{"type":"boolean"},"source_site":{"type":"string"},"token_budget":{"maximum":50000,"minimum":100,"type":"integer"},"user_id":{"description":"Required. user_id.","minLength":1,"type":"string"},"visibility":{"description":"Visibility (one of agent_only / restricted / workspace). Invalid values silently drop to undefined.","enum":["agent_only","restricted","workspace"],"type":"string"},"workspace_id":{"description":"Optional workspace identifier. Silently dropped if empty / non-string.","type":"string"}},"required":["user_id","query"],"type":"object"}}},"required":true}} \ No newline at end of file diff --git a/docs/api-reference/http/update-config.StatusCodes.json b/docs/api-reference/http/update-config.StatusCodes.json index 152c8e7..f3e5015 100644 --- a/docs/api-reference/http/update-config.StatusCodes.json +++ b/docs/api-reference/http/update-config.StatusCodes.json @@ -1 +1 @@ -{"responses":{"200":{"content":{"application/json":{"schema":{"description":"Applied config updates + full post-update snapshot.","properties":{"applied":{"items":{"type":"string"},"type":"array"},"config":{"description":"Runtime config snapshot returned by /health + /config.","properties":{"agentic_retrieval_enabled":{"type":"boolean"},"clarification_conflict_threshold":{"type":"number"},"cross_encoder_enabled":{"type":"boolean"},"embedding_model":{"type":"string"},"embedding_provider":{"type":"string"},"entity_graph_enabled":{"type":"boolean"},"hybrid_search_enabled":{"type":"boolean"},"iterative_retrieval_enabled":{"type":"boolean"},"llm_model":{"type":"string"},"llm_provider":{"type":"string"},"max_search_results":{"type":"number"},"repair_loop_enabled":{"type":"boolean"},"retrieval_profile":{"type":"string"},"voyage_document_model":{"type":"string"},"voyage_query_model":{"type":"string"}},"required":["retrieval_profile","embedding_provider","embedding_model","voyage_document_model","voyage_query_model","llm_provider","llm_model","clarification_conflict_threshold","max_search_results","hybrid_search_enabled","iterative_retrieval_enabled","entity_graph_enabled","cross_encoder_enabled","agentic_retrieval_enabled","repair_loop_enabled"],"type":"object"},"note":{"type":"string"}},"required":["applied","config","note"],"type":"object"}}},"description":"Applied changes + config snapshot."},"400":{"content":{"application/json":{"schema":{"oneOf":[{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object","title":"ErrorBasic"},{"description":"Richer 400 envelope for PUT /v1/memories/config when startup-only fields are included.","example":{"detail":"Fields embedding_provider cannot be mutated at runtime — the embedding/LLM provider caches are fixed at first use.","error":"Provider/model selection is startup-only","rejected":["embedding_provider"]},"properties":{"detail":{"type":"string"},"error":{"type":"string"},"rejected":{"items":{"type":"string"},"type":"array"}},"required":["error","detail","rejected"],"type":"object","title":"ErrorConfig400"}]}}},"description":"Input validation error OR startup-only fields were supplied."},"410":{"content":{"application/json":{"schema":{"description":"410 Gone envelope for PUT /v1/memories/config when runtime mutation is disabled.","example":{"detail":"Set CORE_RUNTIME_CONFIG_MUTATION_ENABLED=true to enable runtime mutation in dev/test environments.","error":"PUT /v1/memories/config is deprecated for production"},"properties":{"detail":{"type":"string"},"error":{"type":"string"}},"required":["error","detail"],"type":"object"}}},"description":"Runtime config mutation is disabled in production."},"500":{"content":{"application/json":{"schema":{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Internal server error"}}} \ No newline at end of file +{"responses":{"200":{"content":{"application/json":{"schema":{"description":"Applied config updates + full post-update snapshot.","properties":{"applied":{"items":{"type":"string"},"type":"array"},"config":{"description":"Runtime config snapshot returned by /health + /config.","properties":{"agentic_retrieval_enabled":{"type":"boolean"},"clarification_conflict_threshold":{"type":"number"},"cross_encoder_enabled":{"type":"boolean"},"embedding_model":{"type":"string"},"embedding_provider":{"type":"string"},"entity_graph_enabled":{"type":"boolean"},"hybrid_search_enabled":{"type":"boolean"},"iterative_retrieval_enabled":{"type":"boolean"},"llm_model":{"type":"string"},"llm_provider":{"type":"string"},"max_search_results":{"type":"number"},"repair_loop_enabled":{"type":"boolean"},"retrieval_profile":{"type":"string"},"voyage_document_model":{"type":"string"},"voyage_query_model":{"type":"string"}},"required":["retrieval_profile","embedding_provider","embedding_model","voyage_document_model","voyage_query_model","llm_provider","llm_model","clarification_conflict_threshold","max_search_results","hybrid_search_enabled","iterative_retrieval_enabled","entity_graph_enabled","cross_encoder_enabled","agentic_retrieval_enabled","repair_loop_enabled"],"type":"object"},"note":{"type":"string"}},"required":["applied","config","note"],"type":"object"}}},"description":"Applied changes + config snapshot."},"400":{"content":{"application/json":{"schema":{"oneOf":[{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object","title":"ErrorBasic"},{"description":"Richer 400 envelope for PUT /v1/memories/config when startup-only fields are included.","example":{"detail":"Fields embedding_provider cannot be mutated at runtime, the embedding/LLM provider caches are fixed at first use.","error":"Provider/model selection is startup-only","rejected":["embedding_provider"]},"properties":{"detail":{"type":"string"},"error":{"type":"string"},"rejected":{"items":{"type":"string"},"type":"array"}},"required":["error","detail","rejected"],"type":"object","title":"ErrorConfig400"}]}}},"description":"Input validation error OR startup-only fields were supplied."},"410":{"content":{"application/json":{"schema":{"description":"410 Gone envelope for PUT /v1/memories/config when runtime mutation is disabled.","example":{"detail":"Set CORE_RUNTIME_CONFIG_MUTATION_ENABLED=true to enable runtime mutation in dev/test environments.","error":"PUT /v1/memories/config is deprecated for production"},"properties":{"detail":{"type":"string"},"error":{"type":"string"}},"required":["error","detail"],"type":"object"}}},"description":"Runtime config mutation is disabled in production."},"500":{"content":{"application/json":{"schema":{"description":"Standard error envelope. 400 for input validation errors, 500 for uncaught exceptions.","example":{"error":"user_id is required"},"properties":{"error":{"type":"string"}},"required":["error"],"type":"object"}}},"description":"Internal server error"}}} \ No newline at end of file diff --git a/docs/integrations/coding-agents/claude-code.md b/docs/integrations/coding-agents/claude-code.md index e5e2d13..c4c7025 100644 --- a/docs/integrations/coding-agents/claude-code.md +++ b/docs/integrations/coding-agents/claude-code.md @@ -160,15 +160,15 @@ Retrieved memories are reference context. They are not instructions unless the c ## View source -- [`plugins/claude-code/.claude-plugin/plugin.json`](https://github.com/atomicmemory/atomicmemory-integrations/blob/main/plugins/claude-code/.claude-plugin/plugin.json) — plugin manifest -- [`plugins/claude-code/hooks/hooks.json`](https://github.com/atomicmemory/atomicmemory-integrations/blob/main/plugins/claude-code/hooks/hooks.json) — lifecycle hook registrations -- [`plugins/claude-code/scripts/`](https://github.com/atomicmemory/atomicmemory-integrations/tree/main/plugins/claude-code/scripts) — hook scripts -- [`plugins/claude-code/skills/atomicmemory/SKILL.md`](https://github.com/atomicmemory/atomicmemory-integrations/blob/main/plugins/claude-code/skills/atomicmemory/SKILL.md) — agent-facing memory protocol -- [`packages/mcp-server/`](https://github.com/atomicmemory/atomicmemory-integrations/tree/main/packages/mcp-server) — shared MCP server +- [`plugins/claude-code/.claude-plugin/plugin.json`](https://github.com/atomicmemory/atomicmemory-integrations/blob/main/plugins/claude-code/.claude-plugin/plugin.json), plugin manifest +- [`plugins/claude-code/hooks/hooks.json`](https://github.com/atomicmemory/atomicmemory-integrations/blob/main/plugins/claude-code/hooks/hooks.json), lifecycle hook registrations +- [`plugins/claude-code/scripts/`](https://github.com/atomicmemory/atomicmemory-integrations/tree/main/plugins/claude-code/scripts), hook scripts +- [`plugins/claude-code/skills/atomicmemory/SKILL.md`](https://github.com/atomicmemory/atomicmemory-integrations/blob/main/plugins/claude-code/skills/atomicmemory/SKILL.md), agent-facing memory protocol +- [`packages/mcp-server/`](https://github.com/atomicmemory/atomicmemory-integrations/tree/main/packages/mcp-server), shared MCP server ## See also -- [SDK Overview](/sdk/overview) — the `MemoryProvider` model behind the plugin -- [Platform scope model](/platform/scope) — how scope fields dispatch -- [Codex integration](/integrations/coding-agents/codex) — skill-only sibling integration -- [OpenClaw integration](/integrations/coding-agents/openclaw) — in-process plugin sibling integration +- [SDK Overview](/sdk/overview), the `MemoryProvider` model behind the plugin +- [Platform scope model](/platform/scope), how scope fields dispatch +- [Codex integration](/integrations/coding-agents/codex), skill-only sibling integration +- [OpenClaw integration](/integrations/coding-agents/openclaw), in-process plugin sibling integration diff --git a/docs/integrations/frameworks/langgraph-js.md b/docs/integrations/frameworks/langgraph-js.md index 98c293d..b2363f5 100644 --- a/docs/integrations/frameworks/langgraph-js.md +++ b/docs/integrations/frameworks/langgraph-js.md @@ -11,7 +11,7 @@ This adapter is on the roadmap. The shape below is the intended API, not a shipp ## Intended shape -LangGraph models agents as state graphs. `@atomicmemory/langgraph` will expose AtomicMemory as a checkpointer-adjacent memory layer — durable semantic memory that lives across graph runs, complementing LangGraph's run-scoped checkpointers: +LangGraph models agents as state graphs. `@atomicmemory/langgraph` will expose AtomicMemory as a checkpointer-adjacent memory layer, durable semantic memory that lives across graph runs, complementing LangGraph's run-scoped checkpointers: ```ts import { StateGraph } from '@langchain/langgraph'; diff --git a/docs/integrations/frameworks/openai-agents.md b/docs/integrations/frameworks/openai-agents.md index 6b68eae..557bae7 100644 --- a/docs/integrations/frameworks/openai-agents.md +++ b/docs/integrations/frameworks/openai-agents.md @@ -24,7 +24,7 @@ const agent = new Agent({ }); ``` -For multi-agent workflows, the adapter also supplies a session handoff that preserves memory scope across agent boundaries — so a routing agent's memory is visible to the specialist agent it hands off to, when scoped the same way. +For multi-agent workflows, the adapter also supplies a session handoff that preserves memory scope across agent boundaries, so a routing agent's memory is visible to the specialist agent it hands off to, when scoped the same way. ## See also diff --git a/docs/introduction.md b/docs/introduction.md index b4213b5..5014aaf 100644 --- a/docs/introduction.md +++ b/docs/introduction.md @@ -5,43 +5,41 @@ sidebar_position: 1 # Introduction -AtomicMemory is an open-source memory engine for AI applications — semantic retrieval, AUDN mutation (Add / Update / Delete / No-op), and contradiction-safe claim versioning, delivered as an HTTP service you can run with one `docker compose up`. It is pluggable at every seam: swap the embedding provider, the LLM, the storage backend, or the scope model without forking. The engine ships as a standardized platform layer — not a framework, not a SaaS — so your agents, assistants, and products can compose the memory stack they need. - -{/* 15-second value prop above; differentiation table below */} +AtomicMemory is an open-source memory engine for AI applications, semantic retrieval, AUDN mutation (Add / Update / Delete / No-op), and contradiction-safe claim versioning, delivered as an HTTP service you can run with one `docker compose up`. It is pluggable at every seam: swap the embedding provider, the LLM, the storage backend, or the scope model without forking. The engine ships as a standardized platform layer, not a framework, not a SaaS, so your agents, assistants, and products can compose the memory stack they need. ## Why AtomicMemory AI memory is becoming a platform concern, not a product feature. Most existing options force a hosted runtime, a specific agent framework, or a proprietary query language. AtomicMemory is designed around the opposite defaults. -| Dimension | AtomicMemory | mem0 | letta | zep | +Memory is also production state. If remembered facts can change agent behavior, engineers need to inspect them, correct them, audit where they came from, and replace the parts of the system they disagree with. Most memory products optimize for recall first and expose operational control later. AtomicMemory is built for memory you can own. + +| Production requirement | AtomicMemory | mem0 | Letta | Zep | |---|---|---|---|---| -| License | Apache 2.0 | Apache 2.0 (OSS + hosted) | Apache 2.0 | Apache 2.0 (OSS + hosted) | -| Primary language | TypeScript (Node, ESM) | Python | Python | Go | -| Deployment | Self-host, Docker Compose, HTTP-first | Self-host OSS + hosted platform | Self-host | Self-host OSS + hosted platform | -| Storage backend | Postgres + pgvector (swappable store interfaces) | Pluggable (multiple vector DBs) | Built-in agent state + vector | Internal store (self-managed) | -| Embedding providers | openai, openai-compatible, ollama, transformers (local WASM), voyage | Multi-provider | Multi-provider | Multi-provider | -| LLM providers | openai, openai-compatible, ollama, anthropic, google, groq | Multi-provider | Multi-provider | Multi-provider | -| Scope model | Explicit `user` / `workspace` / `agent` scopes at the request boundary | User + agent memory | Agent-centric | Session / user centric | -| Observability | Stable `observability` envelope on search responses (retrieval / packaging / assembly) | Logs + integrations | Framework-dependent | Hosted dashboards | -| API surface | HTTP (snake\_case) + [TypeScript SDK](/sdk/overview) | Python + Node SDKs, REST | Python + TypeScript SDKs, REST | Python + TypeScript + Go SDKs, REST | - -Sources for third-party claims: [Mem0 OSS overview](https://docs.mem0.ai/open-source/overview), [Letta intro](https://docs.letta.com/guides/get-started/intro), [Zep overview](https://help.getzep.com/overview). +| Own the memory layer, not rent a hosted opinion | **Best fit.** Apache-2.0 engine, self-hosted by default, Postgres-backed, no required hosted control plane. | OSS plus hosted platform orientation. | Self-hosted agent runtime. | OSS plus commercial / hosted platform orientation. | +| Inspect memory during an incident | **Best fit.** Stored content, trust, source, timestamps, mutation lineage, and audit surfaces live in inspectable engine state and ordinary Postgres. | Inspection depends on Mem0's exposed APIs and platform surfaces. | Inspection happens through the Letta agent/runtime model. | Inspection depends on the server and hosted / platform surfaces. | +| Correct bad memory without wiping the user | **Best fit.** AUDN mutation, `SUPERSEDE`, `CLARIFY`, trust scoring, CRUD, consolidation, and decay are core domains. | Primarily memory add/search/update/delete abstraction. | Memory correction is coupled to agent state. | Memory correction is coupled to the server's memory model. | +| Debug why retrieval behaved a certain way | **Best fit.** Search responses separate retrieval, packaging, and final context assembly in a structured observability envelope. | Logs and integrations. | Framework-dependent tracing. | Hosted / server observability. | +| Swap internals without rewriting the product | **Best fit.** Ingest, Search, CRUD, Lifecycle, Trust, stores, embeddings, LLMs, and SDK backends are explicit typed seams. | Provider and storage choices exist, but the core pipeline is not exposed as five replaceable domains. | Customize by adopting or extending the Letta agent runtime. | Customize mostly through server configuration and supported APIs. | +| Run deterministic local tests and benchmarks | **Best fit.** The same engine boots as production HTTP, in-process TypeScript, or ephemeral test server from `createCoreRuntime`. | Possible, but not the primary product shape. | Possible inside the Letta framework. | Server-oriented; less suited to per-test composition. | +| Keep app code portable across memory engines | **Best fit.** The TypeScript SDK routes through `MemoryProvider`, so apps can compare AtomicMemory, Mem0, or custom backends behind one interface. | You integrate with Mem0's SDK/API. | You integrate with Letta's agent/runtime abstractions. | You integrate with Zep's server/API model. | + +Sources for third-party positioning: [Mem0 OSS overview](https://docs.mem0.ai/open-source/overview), [Letta intro](https://docs.letta.com/guides/get-started/intro), [Zep overview](https://help.getzep.com/overview). AtomicMemory architecture details: [Architecture](/platform/architecture), [Composition](/platform/composition), [Scope](/platform/scope), [Observability](/platform/observability). The pitch is not "we do more." It is: the seams are explicit, the contracts are stable, and you compose your own stack. ## Platform at a glance -- **Pluggable storage** — five domain-facing store interfaces so ingest, search, CRUD, lifecycle, and trust each see only the contract they need ([stores](/platform/stores)) -- **Pluggable providers** — embeddings via openai, openai-compatible, ollama, transformers (local WASM), or voyage; LLM via openai, openai-compatible, ollama, anthropic, google, or groq ([providers](/platform/providers)) -- **Explicit composition** — a single composition root wires the runtime container; no hidden singletons, no global state ([composition](/platform/composition)) -- **First-class scope** — user, workspace, and agent scopes dispatched at the request boundary, not bolted on after ([scope](/platform/scope)) -- **Observability as contract** — every search response carries a stable trace schema so dashboards and evals never break on a refactor ([observability](/platform/observability)) -- **Domain separation** — Ingest, Search, CRUD, Lifecycle, and Trust are independent domains with their own routes and services ([architecture](/platform/architecture)) +- **Pluggable storage**, five domain-facing store interfaces so ingest, search, CRUD, lifecycle, and trust each see only the contract they need ([stores](/platform/stores)) +- **Pluggable providers**, embeddings via openai, openai-compatible, ollama, transformers (local WASM), or voyage; LLM via openai, openai-compatible, ollama, anthropic, google, or groq ([providers](/platform/providers)) +- **Explicit composition**, a single composition root wires the runtime container; no hidden singletons, no global state ([composition](/platform/composition)) +- **First-class scope**, user, workspace, and agent scopes dispatched at the request boundary, not bolted on after ([scope](/platform/scope)) +- **Observability as contract**, every search response carries a stable trace schema so dashboards and evals never break on a refactor ([observability](/platform/observability)) +- **Domain separation**, Ingest, Search, CRUD, Lifecycle, and Trust are independent domains with their own routes and services ([architecture](/platform/architecture)) ## Try it in 2 minutes The fastest path is the [Quickstart](/quickstart): clone the core repo, set an API key, `docker compose up`, and run your first ingest and search with two curl commands. -Core is HTTP-first, so any language works today. The [TypeScript SDK](/sdk/overview) gives TypeScript and JavaScript consumers typed request and response shapes, richer ergonomics, scope-aware helpers, and a pluggable provider model that decouples your app from any particular memory engine — but nothing about core requires it. +Core is HTTP-first, so any language works today. The [TypeScript SDK](/sdk/overview) gives TypeScript and JavaScript consumers typed request and response shapes, richer ergonomics, scope-aware helpers, and a pluggable provider model that decouples your app from any particular memory engine, but nothing about core requires it. -AtomicMemory is [Apache-2.0 licensed](https://github.com/atomicmemory/atomicmemory-core/blob/main/LICENSE). Self-host it, fork it, run it behind your own gateway — the platform is yours. +AtomicMemory is [Apache-2.0 licensed](https://github.com/atomicmemory/atomicmemory-core/blob/main/LICENSE). Self-host it, fork it, run it behind your own gateway, the platform is yours. diff --git a/docs/platform/architecture.md b/docs/platform/architecture.md index 8a60204..c0ecf62 100644 --- a/docs/platform/architecture.md +++ b/docs/platform/architecture.md @@ -4,15 +4,13 @@ title: Architecture # Architecture -AtomicMemory is a **standardized platform layer for AI memory** — not a monolithic service. The engine is split into five independently replaceable domains (Ingest, Search, CRUD, Lifecycle, Trust) that sit behind explicit TypeScript contracts. You can swap any one of them without touching the others, because each is reachable through a plain function signature over a shared `MemoryServiceDeps` bundle. +AtomicMemory is a **standardized platform layer for AI memory**, not a monolithic service. The engine is split into five independently replaceable domains (Ingest, Search, CRUD, Lifecycle, Trust) that sit behind explicit TypeScript contracts. You can swap any one of them without touching the others, because each is reachable through a plain function signature over a shared `MemoryServiceDeps` bundle. This page walks through the five domains, shows where each lives in the real source, and explains why this split matters when you're choosing between AtomicMemory and the single-binary alternatives. -{/* Platform positioning: TS-native, HTTP-first, compose-your-own-stack. */} - ## The five domains -The `MemoryService` class is a **thin facade**. It holds no business logic — every public method delegates to one of five domain modules. Here is the whole shape of the facade (see [`src/services/memory-service.ts`](https://github.com/atomicmemory/atomicmemory-core/blob/main/src/services/memory-service.ts)): +The `MemoryService` class is a **thin facade**. It holds no business logic, every public method delegates to one of five domain modules. Here is the whole shape of the facade (see [`src/services/memory-service.ts`](https://github.com/atomicmemory/atomicmemory-core/blob/main/src/services/memory-service.ts)): ```typescript title="atomicmemory-core/src/services/memory-service.ts" {22-24} import { performIngest, performQuickIngest, performStoreVerbatim, performWorkspaceIngest } from './memory-ingest.js'; @@ -38,7 +36,7 @@ export class MemoryService { } ``` -Every method is a one-liner. The class exists only to resolve the right domain module and pass along `this.deps`. This is deliberate — it means you can invoke any domain *directly* without constructing a `MemoryService` at all, which is exactly what the runtime does internally for parallel or background work. +Every method is a one-liner. The class exists only to resolve the right domain module and pass along `this.deps`. This is deliberate, it means you can invoke any domain *directly* without constructing a `MemoryService` at all, which is exactly what the runtime does internally for parallel or background work. ### 1. Ingest domain @@ -82,7 +80,7 @@ Why is this shaped this way? Because *ingest strategy* is the thing users most w Search is pure orchestration. The file's own header comment is explicit about this: it "delegates formatting to retrieval-format, dedup to composite-dedup, side effects to retrieval-side-effects, lesson recording to lesson-service, and the main retrieval to search-pipeline." Every one of those is a separately replaceable module. -The orchestrator is a sequence of named steps — lesson check, URI resolution, core retrieval, post-processing, response assembly — each one a pure function. You can see the shape clearly in `performSearch`: +The orchestrator is a sequence of named steps, lesson check, URI resolution, core retrieval, post-processing, response assembly, each one a pure function. You can see the shape clearly in `performSearch`: ```typescript title="atomicmemory-core/src/services/memory-search.ts" export async function performSearch(deps, userId, query, /* ... */): Promise { @@ -103,14 +101,14 @@ export async function performSearch(deps, userId, query, /* ... */): Promise Both features are pure functions over memory data + config — they compute what should happen but let the caller decide when to act. +> Both features are pure functions over memory data + config, they compute what should happen but let the caller decide when to act. -That separation — computation of *candidates* from the *action* on them — is what makes lifecycle replaceable. Here is the retention score, the central primitive: +That separation, computation of *candidates* from the *action* on them, is what makes lifecycle replaceable. Here is the retention score, the central primitive: ```typescript title="atomicmemory-core/src/services/memory-lifecycle.ts" export function computeRetentionScore( @@ -160,14 +158,14 @@ export function computeRetentionScore( } ``` -It takes config and a memory row, returns a number. No database, no side effects, no globals. If you want a different decay model — power-law instead of exponential, per-user retention, or a learned importance weighting — you replace this one function and the rest of the engine is unaffected. The same property holds for `checkMemoryCap`, which returns a `CapRecommendation` tagged enum (`'none' | 'consolidate' | 'decay' | 'consolidate-and-decay'`) and leaves the action to the caller. +It takes config and a memory row, returns a number. No database, no side effects, no globals. If you want a different decay model, power-law instead of exponential, per-user retention, or a learned importance weighting, you replace this one function and the rest of the engine is unaffected. The same property holds for `checkMemoryCap`, which returns a `CapRecommendation` tagged enum (`'none' | 'consolidate' | 'decay' | 'consolidate-and-decay'`) and leaves the action to the caller. ### 5. Trust domain **Files:** [`src/services/trust-scoring.ts`](https://github.com/atomicmemory/atomicmemory-core/blob/main/src/services/trust-scoring.ts) + [`src/services/write-security.ts`](https://github.com/atomicmemory/atomicmemory-core/blob/main/src/services/write-security.ts) **Entry points:** `computeTrustScore`, `assessWriteSecurity`, `applyTrustPenalty` -Trust is enforced at two points — **write time** (before a memory is stored) and **read time** (when ranking candidates). At write time, `assessWriteSecurity` is the single gate every ingest path must pass through. It composes sanitization with trust scoring so the standard and hive ingest flows physically cannot diverge on what counts as unsafe content: +Trust is enforced at two points, **write time** (before a memory is stored) and **read time** (when ranking candidates). At write time, `assessWriteSecurity` is the single gate every ingest path must pass through. It composes sanitization with trust scoring so the standard and hive ingest flows physically cannot diverge on what counts as unsafe content: ```typescript title="atomicmemory-core/src/services/write-security.ts" export function assessWriteSecurity( @@ -186,7 +184,7 @@ export function assessWriteSecurity( } ``` -The score itself is built out of three orthogonal signals — domain reputation, injection-pattern detection, and content-anomaly warnings — each of which could be replaced independently: +The score itself is built out of three orthogonal signals, domain reputation, injection-pattern detection, and content-anomaly warnings, each of which could be replaced independently: ```typescript title="atomicmemory-core/src/services/trust-scoring.ts" export function computeTrustScore(content: string, sourceSite: string): TrustScore { @@ -206,7 +204,7 @@ export function computeTrustScore(content: string, sourceSite: string): TrustSco } ``` -At read time, the same trust score is re-used to down-rank low-trust memories via `applyTrustPenalty(retrievalScore, trustScore)`. One number, computed once at write, referenced everywhere after. That is what lets you enable or disable the whole domain with a single config flag — and it is what makes it safe to replace the scorer without re-embedding or re-ingesting. +At read time, the same trust score is re-used to down-rank low-trust memories via `applyTrustPenalty(retrievalScore, trustScore)`. One number, computed once at write, referenced everywhere after. That is what lets you enable or disable the whole domain with a single config flag, and it is what makes it safe to replace the scorer without re-embedding or re-ingesting. ## The shared contract: `MemoryServiceDeps` @@ -219,7 +217,7 @@ async function listMemories(deps: MemoryServiceDeps, /* ... */) async function evaluateDecay(deps: MemoryServiceDeps, /* ... */): Promise ``` -`MemoryServiceDeps` is the full dependency bundle — `config`, `stores` (memory / episode / search / link / representation / claim / entity / lesson / pool), `observationService`, `uriResolver`. Because it is the *same* type for every domain, a custom implementation of any domain slots in without adapter code. And because each store is an interface (not a class), swapping Postgres for an alternative backend is a matter of implementing the store contract, not rewriting the engine. +`MemoryServiceDeps` is the full dependency bundle, `config`, `stores` (memory / episode / search / link / representation / claim / entity / lesson / pool), `observationService`, `uriResolver`. Because it is the *same* type for every domain, a custom implementation of any domain slots in without adapter code. And because each store is an interface (not a class), swapping Postgres for an alternative backend is a matter of implementing the store contract, not rewriting the engine. See the [stores](./stores.md) page for the full store contracts and how to bind custom implementations. @@ -227,15 +225,15 @@ See the [stores](./stores.md) page for the full store contracts and how to bind **vs. mem0.** Mem0 is SaaS-first and Python-centric. The open-source tier is a thin wrapper around their hosted API; the "platform" is hosted, not composed. AtomicMemory's engine runs on your Postgres, is addressable via both HTTP and in-process TypeScript, and lets you replace any of the five domains with your own implementation. There is no hosted dependency on the critical path. -**vs. Letta (formerly MemGPT).** Letta is tightly coupled to an agent framework — memory is a feature of the agent runtime, not a standalone service. If you're not using Letta's agents, you inherit a lot of framework you don't want. AtomicMemory's five-domain split is agent-framework-agnostic: the Ingest domain does not know what called it, the Search domain returns a `RetrievalResult`, and you wire it into whatever agent layer you already have. +**vs. Letta (formerly MemGPT).** Letta is tightly coupled to an agent framework, memory is a feature of the agent runtime, not a standalone service. If you're not using Letta's agents, you inherit a lot of framework you don't want. AtomicMemory's five-domain split is agent-framework-agnostic: the Ingest domain does not know what called it, the Search domain returns a `RetrievalResult`, and you wire it into whatever agent layer you already have. **vs. Zep.** Zep is a Go-based commercial server with a fixed internal architecture. Extending it means forking Go code or waiting for upstream features. AtomicMemory is TypeScript-native with explicit domain boundaries and typed store contracts, so extending it means writing a TypeScript module that satisfies an existing interface. The cost-of-customization curve is fundamentally different. -The common theme: AtomicMemory treats memory as a **pluggable platform layer**, not a product. Every seam is explicit, typed, and individually replaceable — which is what makes the engine suitable as a foundation for teams who intend to customize, not just consume. +The common theme: AtomicMemory treats memory as a **pluggable platform layer**, not a product. Every seam is explicit, typed, and individually replaceable, which is what makes the engine suitable as a foundation for teams who intend to customize, not just consume. ## Next steps -- [Composition](./composition.md) — how `createCoreRuntime`, `createApp`, and `bindEphemeral` let you boot the engine in any context -- [Stores](./stores.md) — the pluggable storage contracts each domain runs on -- [Providers](./providers.md) — swapping embeddings and LLMs -- [Scope](./scope.md) — user, workspace, and agent-visibility boundaries +- [Composition](./composition.md), how `createCoreRuntime`, `createApp`, and `bindEphemeral` let you boot the engine in any context +- [Stores](./stores.md), the pluggable storage contracts each domain runs on +- [Providers](./providers.md), swapping embeddings and LLMs +- [Scope](./scope.md), user, workspace, and agent-visibility boundaries diff --git a/docs/platform/composition.md b/docs/platform/composition.md index b3975ad..c814bb6 100644 --- a/docs/platform/composition.md +++ b/docs/platform/composition.md @@ -4,15 +4,13 @@ title: Composition # Composition -AtomicMemory has an **explicit composition root** — a single function, `createCoreRuntime`, that owns every piece of wiring between config, the Postgres pool, repositories, stores, and services. On top of that root, a three-seam design (`createCoreRuntime` → `createApp` → `bindEphemeral`) lets the same engine boot into production HTTP, in-process TypeScript, and ephemeral test harnesses **without forking code paths**. This is the property that makes AtomicMemory usable as a platform layer and not just a server. +AtomicMemory has an **explicit composition root**, a single function, `createCoreRuntime`, that owns every piece of wiring between config, the Postgres pool, repositories, stores, and services. On top of that root, a three-seam design (`createCoreRuntime` → `createApp` → `bindEphemeral`) lets the same engine boot into production HTTP, in-process TypeScript, and ephemeral test harnesses **without forking code paths**. This is the property that makes AtomicMemory usable as a platform layer and not just a server. This page shows what each seam does, how they compose, and why this shape is a differentiator when you look at the alternatives. -{/* Three seams: runtime (compose), app (wire HTTP), ephemeral (bind port). */} - ## The three seams -Every consumer of AtomicMemory — the production server, the test suite, a research benchmark, an embedded agent harness — boots through some subset of these three calls: +Every consumer of AtomicMemory, the production server, the test suite, a research benchmark, an embedded agent harness, boots through some subset of these three calls: ```typescript const runtime = createCoreRuntime({ pool }); // seam 1: compose deps into a runtime @@ -33,13 +31,13 @@ Each seam is a plain function. No global state, no hidden DI container, no frame **File:** [`src/app/runtime-container.ts`](https://github.com/atomicmemory/atomicmemory-core/blob/main/src/app/runtime-container.ts) -`createCoreRuntime` is the composition root. It takes an explicit dependency bundle — the Postgres pool plus an optional composition-time config for isolated harnesses — and returns a fully wired `CoreRuntime` containing config, repos, stores, and services: +`createCoreRuntime` is the composition root. It takes an explicit dependency bundle, the Postgres pool plus an optional composition-time config for isolated harnesses, and returns a fully wired `CoreRuntime` containing config, repos, stores, and services: ```typescript title="atomicmemory-core/src/app/runtime-container.ts" /** * Explicit dependency bundle accepted by `createCoreRuntime`. * - * `pool` is required — the composition root never reaches around to + * `pool` is required, the composition root never reaches around to * import the singleton `pg.Pool` itself. * * Optional `config` is for isolated harnesses such as benchmark runs. @@ -50,7 +48,7 @@ export interface CoreRuntimeDeps { config?: RuntimeConfig; } -/** The composed runtime — single source of truth for route registration. */ +/** The composed runtime, single source of truth for route registration. */ export interface CoreRuntime { config: RuntimeConfig; configRouteAdapter: CoreRuntimeConfigRouteAdapter; @@ -101,11 +99,11 @@ export function createCoreRuntime(deps: CoreRuntimeDeps): CoreRuntime { Three things are important about this shape: -**It takes `pool` as an argument.** The comment is explicit: *"the composition root never reaches around to import the singleton `pg.Pool` itself."* This is a rule, not an accident. It means tests can pass a test-scoped pool, research harnesses can pass a benchmark-scoped pool, and the production server can pass its production pool — all through the same entry point. No global Pool importer anywhere under `app/`. +**It takes `pool` as an argument.** The comment is explicit: *"the composition root never reaches around to import the singleton `pg.Pool` itself."* This is a rule, not an accident. It means tests can pass a test-scoped pool, research harnesses can pass a benchmark-scoped pool, and the production server can pass its production pool, all through the same entry point. No global Pool importer anywhere under `app/`. -**It returns a typed `CoreRuntime`.** Everything downstream — route registration, in-process calls, store access — goes through this typed bundle. There is no "god object" or service locator. If a piece of code wants the memory service, it reads `runtime.services.memory`. If it wants the claim repository for a custom query, it reads `runtime.repos.claims`. The handles are explicit and narrow. +**It returns a typed `CoreRuntime`.** Everything downstream, route registration, in-process calls, store access, goes through this typed bundle. There is no "god object" or service locator. If a piece of code wants the memory service, it reads `runtime.services.memory`. If it wants the claim repository for a custom query, it reads `runtime.repos.claims`. The handles are explicit and narrow. -**Optional domains are wired as `null`, not absent.** When `entityGraphEnabled` is false, the runtime stores `entities: null` rather than omitting the field. Downstream code checks `if (!deps.stores.entity) return null;` — the contract is uniform and the disablement is discoverable, not accidental. +**Optional domains are wired as `null`, not absent.** When `entityGraphEnabled` is false, the runtime stores `entities: null` rather than omitting the field. Downstream code checks `if (!deps.stores.entity) return null;`, the contract is uniform and the disablement is discoverable, not accidental. ## Seam 2: `createApp` @@ -139,7 +137,7 @@ export function createApp(runtime: CoreRuntime): ReturnType { The file header makes the boundary explicit: *"Separates composition (done in `runtime-container.ts`) from HTTP transport concerns. Tests and harnesses can create an Express app from any runtime container without touching the server bootstrap."* -That is the key property. Because `createApp` takes an already-composed runtime, you can point HTTP at *any* runtime — a test runtime with a scratch schema, a research runtime with a different config, a runtime with custom stores bound — and the routes don't know or care. Conversely, because composition happens in `createCoreRuntime`, you can use the runtime *without* `createApp` at all when you want to skip HTTP and call services directly in process. +That is the key property. Because `createApp` takes an already-composed runtime, you can point HTTP at *any* runtime, a test runtime with a scratch schema, a research runtime with a different config, a runtime with custom stores bound, and the routes don't know or care. Conversely, because composition happens in `createCoreRuntime`, you can use the runtime *without* `createApp` at all when you want to skip HTTP and call services directly in process. The two seams compose, but they don't *require* each other. That is the whole point. @@ -167,7 +165,7 @@ export async function bindEphemeral(app: ReturnType): Promise { }); ``` -This test does two useful things at once. It documents the canonical in-process and HTTP call patterns — a new consumer can read this file and immediately see how to use the engine. And it makes "HTTP and in-process agree on stored state" into a machine-checked invariant instead of a README claim. The day someone accidentally adds a caching layer that makes the HTTP seam see stale data, this test fails. +This test does two useful things at once. It documents the canonical in-process and HTTP call patterns, a new consumer can read this file and immediately see how to use the engine. And it makes "HTTP and in-process agree on stored state" into a machine-checked invariant instead of a README claim. The day someone accidentally adds a caching layer that makes the HTTP seam see stale data, this test fails. ## Why this shape is a differentiator **No hidden singletons.** The `server.ts` file for the production server is fifteen lines of top-level code: import pool, call `createCoreRuntime({ pool })`, call `createApp(runtime)`, listen. Every dependency is threaded explicitly. You can audit the entire boot path by reading two files. -**Tests boot the real engine.** Integration tests don't use mocks-of-mocks. They call `createCoreRuntime` with a test pool, `createApp` on the runtime, and `bindEphemeral` to get a URL. The code under test is bit-for-bit the code running in production — only the pool and config are scoped differently. +**Tests boot the real engine.** Integration tests don't use mocks-of-mocks. They call `createCoreRuntime` with a test pool, `createApp` on the runtime, and `bindEphemeral` to get a URL. The code under test is bit-for-bit the code running in production, only the pool and config are scoped differently. -**Research harnesses are first-class.** The [Consuming core](/platform/consuming-core) guide documents three official consumption modes — HTTP, in-process runtime container, and docker/E2E compose — and the composition root exposes them all through the same three seams. A research benchmark can run 10,000 in-process ingests without paying HTTP overhead, then flip to HTTP for a parity check, using the same runtime. +**Research harnesses are first-class.** The [Consuming core](/platform/consuming-core) guide documents three official consumption modes, HTTP, in-process runtime container, and docker/E2E compose, and the composition root exposes them all through the same three seams. A research benchmark can run 10,000 in-process ingests without paying HTTP overhead, then flip to HTTP for a parity check, using the same runtime. ## Why this matters vs the alternatives -**vs. mem0.** Mem0's OSS library is a client wrapping a hosted service, or a single-process Python runtime with implicit global state. There is no "bind an ephemeral app to a test pool" story — running an integration test against mem0 means either talking to their cloud or fighting their module-level singletons. AtomicMemory's three-seam design makes local, parallel, and test boots trivial. +**vs. mem0.** Mem0's OSS library is a client wrapping a hosted service, or a single-process Python runtime with implicit global state. There is no "bind an ephemeral app to a test pool" story, running an integration test against mem0 means either talking to their cloud or fighting their module-level singletons. AtomicMemory's three-seam design makes local, parallel, and test boots trivial. -**vs. Letta.** Letta's architecture assumes you're running the Letta agent server. Its memory layer is reachable primarily *through* that server. AtomicMemory's `createCoreRuntime` gives you the memory engine as a pure TypeScript object with typed services — you can embed it in a Next.js API route, a long-running worker, a Cloudflare Worker, or a Vitest test with exactly the same code. +**vs. Letta.** Letta's architecture assumes you're running the Letta agent server. Its memory layer is reachable primarily *through* that server. AtomicMemory's `createCoreRuntime` gives you the memory engine as a pure TypeScript object with typed services, you can embed it in a Next.js API route, a long-running worker, a Cloudflare Worker, or a Vitest test with exactly the same code. -**vs. Zep.** Zep is a Go binary. Composition is whatever the binary does; the only customization surface is config flags. AtomicMemory is a TypeScript library that happens to also run an Express server. The composition root is source code you can read, fork, and adapt — not a deploy artifact. +**vs. Zep.** Zep is a Go binary. Composition is whatever the binary does; the only customization surface is config flags. AtomicMemory is a TypeScript library that happens to also run an Express server. The composition root is source code you can read, fork, and adapt, not a deploy artifact. The deeper point: **every boundary is a plain function with a typed argument and a typed return**. `createCoreRuntime({ pool })` returns a `CoreRuntime`. `createApp(runtime)` returns an Express app. `bindEphemeral(app)` returns a `BootedApp`. There is no hidden state between them, which means there is no hidden cost to replacing any one of them. @@ -239,7 +237,7 @@ That is what "standardized platform layer, pluggable at every seam" actually mea ## Next steps -- [Architecture](./architecture.md) — the five domains that live inside `runtime.services.memory` -- [Stores](./stores.md) — how `CoreStores` lets you swap storage backends -- [Providers](./providers.md) — how `initEmbedding` and `initLlm` let you swap providers at composition time -- [Observability](./observability.md) — tracing, timing, and audit events surfaced through the runtime +- [Architecture](./architecture.md), the five domains that live inside `runtime.services.memory` +- [Stores](./stores.md), how `CoreStores` lets you swap storage backends +- [Providers](./providers.md), how `initEmbedding` and `initLlm` let you swap providers at composition time +- [Observability](./observability.md), tracing, timing, and audit events surfaced through the runtime diff --git a/docs/platform/consuming-core.md b/docs/platform/consuming-core.md index 2772381..6107c66 100644 --- a/docs/platform/consuming-core.md +++ b/docs/platform/consuming-core.md @@ -81,9 +81,9 @@ res.headers.get('X-Atomicmem-Config-Override-Keys'); // 'hybridSearchEnable res.headers.get('X-Atomicmem-Unknown-Override-Keys'); // null unless a key doesn't match a current RuntimeConfig field ``` -Shape: a flat object whose values are primitives (boolean, number, string, null). Keys should be `RuntimeConfig` field names. The schema is permissive: unknown keys are accepted and carried through, and surface via the `X-Atomicmem-Unknown-Override-Keys` response header plus a server-side warning log rather than a 400 — so adding a new overlay-eligible `RuntimeConfig` field in a future release doesn't require a matching schema landing before consumers can use it. +Shape: a flat object whose values are primitives (boolean, number, string, null). Keys should be `RuntimeConfig` field names. The schema is permissive: unknown keys are accepted and carried through, and surface via the `X-Atomicmem-Unknown-Override-Keys` response header plus a server-side warning log rather than a 400, so adding a new overlay-eligible `RuntimeConfig` field in a future release doesn't require a matching schema landing before consumers can use it. -Effective config semantics: `{ ...startup, ...override }` (shallow merge). `maxSearchResults` in an override is fully respected — the request-limit clamp uses the post-override value, not the startup snapshot. +Effective config semantics: `{ ...startup, ...override }` (shallow merge). `maxSearchResults` in an override is fully respected, the request-limit clamp uses the post-override value, not the startup snapshot. ## In-process @@ -131,9 +131,9 @@ const read = await runtime.services.memory.search('alice', 'what stack?'); Stable imports from the root export: - `createCoreRuntime`, `CoreRuntime`, `CoreRuntimeDeps` -- `createApp` — build the Express app from a runtime -- `bindEphemeral` — bind the app to an ephemeral port (for tests) -- `checkEmbeddingDimensions` — startup guard +- `createApp`, build the Express app from a runtime +- `bindEphemeral`, bind the app to an ephemeral port (for tests) +- `checkEmbeddingDimensions`, startup guard - `MemoryService`, `IngestResult`, `RetrievalResult` **Config caveat.** `createCoreRuntime({ pool, config })` is intended for isolated single-runtime harnesses such as benchmark runs. Embedding and LLM modules still hold module-local provider state, so do not keep two concurrently-active runtimes with different embedding/LLM configs in one Node process. Use a fresh process or fresh isolated runtime lifecycle per provider/model configuration. @@ -144,9 +144,9 @@ The canonical compose file for isolated end-to-end runs is `docker-compose.smoke Key env overrides: -- `APP_PORT` (default `3061`) — host port bound to the core container's 3050 -- `POSTGRES_PORT` (default `5444`) — host port for the pgvector container -- `EMBEDDING_PROVIDER` / `EMBEDDING_MODEL` / `EMBEDDING_DIMENSIONS` — already wired to `transformers` / `Xenova/all-MiniLM-L6-v2` / `384` for offline runs +- `APP_PORT` (default `3061`), host port bound to the core container's 3050 +- `POSTGRES_PORT` (default `5444`), host port for the pgvector container +- `EMBEDDING_PROVIDER` / `EMBEDDING_MODEL` / `EMBEDDING_DIMENSIONS`, already wired to `transformers` / `Xenova/all-MiniLM-L6-v2` / `384` for offline runs Use this mode for extension E2E, release validation, or any harness that needs to treat core exactly as it ships. @@ -159,10 +159,10 @@ Use this mode for extension E2E, release validation, or any harness that needs t Two service modules hold config as module-local state and require an explicit init before their hot-path APIs work: -- `@atomicmemory/atomicmemory-core/services/embedding` — `embedText` / `embedTexts` throw unless `initEmbedding(config)` has been called. -- `@atomicmemory/atomicmemory-core/services/llm` — the `llm` / `createLLMProvider` APIs throw unless `initLlm(config)` has been called. +- `@atomicmemory/atomicmemory-core/services/embedding`, `embedText` / `embedTexts` throw unless `initEmbedding(config)` has been called. +- `@atomicmemory/atomicmemory-core/services/llm`, the `llm` / `createLLMProvider` APIs throw unless `initLlm(config)` has been called. -**Consumers going through `createCoreRuntime({ pool })` are auto-initialized** — the composition root calls both inits internally. If you deep-import these modules directly (unstable path), you must call the init yourself: +**Consumers going through `createCoreRuntime({ pool })` are auto-initialized**, the composition root calls both inits internally. If you deep-import these modules directly (unstable path), you must call the init yourself: ```ts import { @@ -177,26 +177,26 @@ initLlm(config); // Now embedText / embedTexts / llm.chat work. ``` -`initEmbedding`, `initLlm`, `EmbeddingConfig`, and `LLMConfig` are re-exported from the root for this purpose. Explicit init is the preferred pattern — the modules will throw with an actionable error message if you forget. +`initEmbedding`, `initLlm`, `EmbeddingConfig`, and `LLMConfig` are re-exported from the root for this purpose. Explicit init is the preferred pattern, the modules will throw with an actionable error message if you forget. Rationale: provider/model selection is startup-only, so module-local state after an explicit init matches the effective contract without the cross-module coupling to `config.ts`. ## Config surface: supported vs experimental -Runtime config is split into two contracts. The split is documented in `src/config.ts` via `SUPPORTED_RUNTIME_CONFIG_FIELDS` and `INTERNAL_POLICY_CONFIG_FIELDS`. A partition test (`src/__tests__/config-partition.test.ts`) enforces disjointness and full coverage — any new `RuntimeConfig` field must be tagged into one bucket. +Runtime config is split into two contracts. The split is documented in `src/config.ts` via `SUPPORTED_RUNTIME_CONFIG_FIELDS` and `INTERNAL_POLICY_CONFIG_FIELDS`. A partition test (`src/__tests__/config-partition.test.ts`) enforces disjointness and full coverage, any new `RuntimeConfig` field must be tagged into one bucket. -- **`SupportedRuntimeConfig`** — fields with a stable contract. Consumers may rely on their semantics, defaults, and presence. Breaking changes go through a documented deprecation cycle. This is where infrastructure (database, port), provider/model selection (embedding, LLM, cross-encoder), and major feature toggles (entity graph, lessons, repair loop, agentic retrieval, etc.) live. -- **`InternalPolicyConfig`** — experimental / tuning flags. Thresholds, scoring weights, MMR/PPR lambdas, staging internals, affinity-clustering knobs, entropy-gate parameters, composite-grouping parameters, etc. **No stability guarantee.** These may be renamed, re-defaulted, or removed between minor versions. Consumers must not persist values in deployment configs expecting them to remain meaningful. Promoted to the supported set when a field's behavior stabilizes. +- **`SupportedRuntimeConfig`**, fields with a stable contract. Consumers may rely on their semantics, defaults, and presence. Breaking changes go through a documented deprecation cycle. This is where infrastructure (database, port), provider/model selection (embedding, LLM, cross-encoder), and major feature toggles (entity graph, lessons, repair loop, agentic retrieval, etc.) live. +- **`InternalPolicyConfig`**, experimental / tuning flags. Thresholds, scoring weights, MMR/PPR lambdas, staging internals, affinity-clustering knobs, entropy-gate parameters, composite-grouping parameters, etc. **No stability guarantee.** These may be renamed, re-defaulted, or removed between minor versions. Consumers must not persist values in deployment configs expecting them to remain meaningful. Promoted to the supported set when a field's behavior stabilizes. Both types are re-exported from the root package. Docs, code review, and release notes should reference `SUPPORTED_RUNTIME_CONFIG_FIELDS` as the authoritative list of what's stable. -### `PUT /v1/memories/config` — dev/test only +### `PUT /v1/memories/config`, dev/test only `PUT /v1/memories/config` is gated by the startup-validated flag `runtimeConfigMutationEnabled` (env: `CORE_RUNTIME_CONFIG_MUTATION_ENABLED`). - **Production** deploys leave the flag unset → the route returns `410 Gone`. Production config must come from env vars at process start, not runtime HTTP mutation. - **Dev / test** deploys set `CORE_RUNTIME_CONFIG_MUTATION_ENABLED=true` → the route mutates the runtime singleton. `.env.test` has this set by default so local test runs and CI continue to work. -Even in dev/test, provider/model fields (`embedding_provider`, `embedding_model`, `voyage_api_key`, `voyage_document_model`, `voyage_query_model`, `llm_provider`, `llm_model`) are rejected with 400 — these are composition-time because the embedding/LLM provider caches are fixed for a runtime. Set them via env vars before server startup, or pass a full `RuntimeConfig` when creating an isolated in-process runtime. Only `similarity_threshold`, `audn_candidate_threshold`, `clarification_conflict_threshold`, and `max_search_results` are mutable through this route. +Even in dev/test, provider/model fields (`embedding_provider`, `embedding_model`, `voyage_api_key`, `voyage_document_model`, `voyage_query_model`, `llm_provider`, `llm_model`) are rejected with 400, these are composition-time because the embedding/LLM provider caches are fixed for a runtime. Set them via env vars before server startup, or pass a full `RuntimeConfig` when creating an isolated in-process runtime. Only `similarity_threshold`, `audn_candidate_threshold`, `clarification_conflict_threshold`, and `max_search_results` are mutable through this route. -Routes read the flag from a memoized startup snapshot through `configRouteAdapter.current().runtimeConfigMutationEnabled` — they never re-check `process.env` at request time, matching the workspace rule that config is validated once at startup. +Routes read the flag from a memoized startup snapshot through `configRouteAdapter.current().runtimeConfigMutationEnabled`, they never re-check `process.env` at request time, matching the workspace rule that config is validated once at startup. diff --git a/docs/platform/observability.md b/docs/platform/observability.md index 63b8577..4114877 100644 --- a/docs/platform/observability.md +++ b/docs/platform/observability.md @@ -8,7 +8,7 @@ Every AtomicMemory retrieval emits a structured, stable observability payload describing *what happened*: which candidates came back from the index, which ones were packaged into the final context, and how the assembled block fit inside the token budget. This is not a debug -log — it is a contract surface on the search response, safe to build +log, it is a contract surface on the search response, safe to build dashboards, evals, and regression tests against. Three summaries, each with a single job: @@ -19,18 +19,11 @@ Three summaries, each with a single job: | `packagingSummary` | Which of those made it into the context, in what role? | | `assemblySummary` | How were they laid out against the token budget? | -{/* - Why this exists as a first-class contract: competing memory systems - either surface nothing (zep, letta) or dump free-form logs that - aren't safe to parse (mem0). We treat the observability fields the - same way we treat the memories themselves — stable shape, versioned, - part of the HTTP response, not an opt-in trace artifact. -*/} ## The summary shapes All three summaries are declared together in `retrieval-trace.ts`. They -are plain data — no functions, no hidden references — and they're +are plain data, no functions, no hidden references, and they're serialized straight into JSON responses. ```ts title="atomicmemory-core/src/services/retrieval-trace.ts:51-79" @@ -71,23 +64,23 @@ export interface AssemblyTraceSummary { Each field is there to answer a specific question a dashboard, eval harness, or prompt-engineer would ask: -- **`candidateIds` vs `includedIds` vs `finalIds`** — the three +- **`candidateIds` vs `includedIds` vs `finalIds`**, the three snapshots of "what's in the bag" at the boundaries between retrieval → packaging → assembly. Diffing them tells you exactly where a memory was dropped. -- **`traceId`, `stageCount`, and `stageNames`** — the bridge from +- **`traceId`, `stageCount`, and `stageNames`**, the bridge from stable response summaries to the optional full trace artifact. When trace persistence is enabled, `traceId` names the on-disk JSON file; `stageNames` gives dashboards a cheap stage inventory without opening that file. -- **`evidenceRoles`** — maps each included memory ID to its role +- **`evidenceRoles`**, maps each included memory ID to its role (`primary` / `supporting` / `historical` / `contextual`). This is how packaging communicates "this is the answer, these are the witnesses" downstream. -- **`tokenCost`** and **`tokenBudget`** — what we used, what was +- **`tokenCost`** and **`tokenBudget`**, what we used, what was allowed. If `finalTokenCost > tokenBudget`, the assembly forced a drop; if it's well under, there's headroom. -- **`primaryEvidencePosition`** — 1-indexed position of the first +- **`primaryEvidencePosition`**, 1-indexed position of the first primary-role memory in the final block. Lower is better (answer at the top), `null` means no primary evidence found. @@ -168,13 +161,13 @@ Two behaviors worth calling out: fine-grained forensic log (saved to disk when tracing is on, for eval runs). Summaries are the stable contract (attached to every response when present). Disabling trace persistence does not - disable summaries — summaries travel with the search result + disable summaries, summaries travel with the search result regardless. ## Packaging → summaries in one call The packaging pipeline builds the packaging and assembly summaries -together — they're two slices of the same computation — and hands +together, they're two slices of the same computation, and hands them back to the caller in a single invocation. This is the seam between "retrieval produced candidates" and "we have an assembled, token-budgeted context block." @@ -184,7 +177,7 @@ token-budgeted context block." * Build packaging + assembly summaries, emit the tiered-packaging trace * event when in tiered mode, and attach both summaries to the active * trace. Returns the summaries so the caller can include them in the - * retrieval result. Does NOT finalize the trace — caller owns that. + * retrieval result. Does NOT finalize the trace, caller owns that. */ export function finalizePackagingTrace( activeTrace: TraceCollector, @@ -220,7 +213,7 @@ export function finalizePackagingTrace( The function is deliberately disciplined: it computes the two summaries, emits the one tiered-mode event needed for offline analysis, attaches both summaries to the active trace, and returns -them. It does not finalize the trace — the search pipeline owns that +them. It does not finalize the trace, the search pipeline owns that call, so the summaries can be attached whether or not the trace is persisted. @@ -249,8 +242,8 @@ export interface RetrievalResult { } ``` -Any consumer of the SDK — whether they're calling `MemoryService` -directly or talking to the HTTP API — sees the same three summaries. +Any consumer of the SDK, whether they're calling `MemoryService` +directly or talking to the HTTP API, sees the same three summaries. The in-process shape uses TypeScript camelCase. The HTTP API translates that payload to the public snake_case wire convention. @@ -268,7 +261,7 @@ export interface RetrievalObservability { } ``` -The route builder is a thin shim — it collects whichever summaries +The route builder is a thin shim, it collects whichever summaries are present on the `RetrievalResult` and omits the whole field if none of them ran. @@ -355,14 +348,14 @@ response: miscalibrated for your model's context window. None of these require turning on a trace flag or mining a separate -log store — the contract is in the response. +log store, the contract is in the response. ## Relationship to on-disk traces The `TraceCollector` can also persist full per-stage traces (with content previews, similarities, scores, and per-stage timestamps) to disk when `RETRIEVAL_TRACE_ENABLED=true`. Those files are the -fine-grained forensic artifact — useful for offline eval runs, +fine-grained forensic artifact, useful for offline eval runs, regression archaeology, and prompt-engineering drills. They are a superset of what the summaries expose, and they live in `docs/memory-research/evaluation/traces/` by default. @@ -375,7 +368,7 @@ Summaries and on-disk traces are complementary, not redundant: previews, per-stage snapshots). Tooling-friendly for eval pipelines. Not part of the API contract. -If you need to answer "what happened on this one request?" — use the +If you need to answer "what happened on this one request?", use the response's `observability` field. If you need to answer "across 10k -requests last week, where did we drop candidates?" — turn on +requests last week, where did we drop candidates?", turn on `RETRIEVAL_TRACE_ENABLED` for the relevant job and mine the traces. diff --git a/docs/platform/providers.md b/docs/platform/providers.md index ef82b28..8816ff2 100644 --- a/docs/platform/providers.md +++ b/docs/platform/providers.md @@ -4,12 +4,10 @@ title: Providers # Providers -> **Disambiguation.** This page is about **embedding and LLM providers** inside the engine — OpenAI, Ollama, Anthropic, etc. The SDK has a separate concept called **memory providers** (`MemoryProvider` — the interface a memory backend implements so the SDK can route through it). Different layer, different concept. See the SDK's [memory providers](/sdk/concepts/provider-model). - Embeddings and LLM calls in AtomicMemory are **pluggable providers behind single-method interfaces**. You pick OpenAI, Anthropic, Google, Groq, Ollama, a local WASM model, or any OpenAI-compatible endpoint at deploy time via -environment variables. Nothing above the provider boundary changes — no code, +environment variables. Nothing above the provider boundary changes, no code, no imports, no service wiring. That is the second pillar of the platform layer: the services that call @@ -17,11 +15,6 @@ That is the second pillar of the platform layer: the services that call serving the call. The selection is made once at composition-root time and erased behind an interface. -{/* Competitive angle: mem0 leans on managed OpenAI + their SaaS. letta - couples model choice to the agent framework. zep is a Go runtime with - its own model stack. AtomicMemory treats the model layer as a - commodity — TypeScript-native, HTTP-first, bring your own. */} - ## The two interfaces Both provider families are tiny. That's deliberate: the less surface the @@ -43,9 +36,9 @@ export interface EmbeddingProvider { Two methods. One for single embeddings, one for batch. The `task` argument lets providers apply query/document-specific behavior without leaking provider -details into ingest or search. Every backend — OpenAI REST, Ollama's native +details into ingest or search. Every backend, OpenAI REST, Ollama's native API, local WASM via transformers.js, Voyage AI, any OpenAI-compatible endpoint -— satisfies exactly this shape. +, satisfies exactly this shape. ### `LLMProvider` @@ -70,7 +63,7 @@ export interface LLMProvider { ``` One method. Chat messages in, completion text out. JSON mode, seed, and -temperature are passed as options — any backend that can't honor one of them +temperature are passed as options, any backend that can't honor one of them (for instance, Anthropic ignores `seed`) degrades gracefully inside the adapter, never leaking the difference to callers. @@ -209,7 +202,7 @@ Two things to notice: 1. **Three providers (Groq, Google Gemini, OpenAI-compatible) reuse `OpenAICompatibleLLM`.** The OpenAI SDK's wire format is the industry default, so the adapter is written once and pointed at different - `baseURL`s. That's what "openai-compatible" costs us — nothing. + `baseURL`s. That's what "openai-compatible" costs us, nothing. 2. **Anthropic gets its own adapter** because the Messages API has a different message shape (system prompt is top-level, assistant/user messages are separate). The adapter normalizes it to @@ -224,7 +217,7 @@ This is the headline. Here's the ingest pipeline calling `embedText`: ```ts import { embedText } from './services/embedding.js'; -// Inside the ingest service — no provider name anywhere. +// Inside the ingest service, no provider name anywhere. const embedding = await embedText(userMessage, 'document'); await stores.memory.storeMemory({ userId, content: userMessage, embedding, importance, sourceSite, @@ -275,7 +268,7 @@ VOYAGE_API_KEY=pa-… ``` The ingest service, the search service, the AUDN decision loop, the repair -loop — none of them have a single `if (provider === 'ollama')` branch. That +loop, none of them have a single `if (provider === 'ollama')` branch. That is the provider-agnostic boundary working as designed. --- @@ -283,7 +276,7 @@ is the provider-agnostic boundary working as designed. ## Provider quirks stay inside the adapter Being agnostic doesn't mean being naïve. Real embedding models have -real quirks, and the adapter layer is where those quirks live — never above. +real quirks, and the adapter layer is where those quirks live, never above. Two examples from the shipped code: ### Instruction prefixes @@ -312,7 +305,7 @@ any) is model-specific, and the logic lives in one place. ### ONNX Runtime serialization (WASM provider) -The local WASM provider has a known concurrency issue — ONNX Runtime's +The local WASM provider has a known concurrency issue, ONNX Runtime's mutex corrupts under concurrent async calls. Rather than leak a "don't call concurrently" caveat into every consumer, the adapter serializes internally. From [`embedding.ts:168-218`](https://github.com/atomicmemory/atomicmemory-core/blob/main/src/services/embedding.ts#L168-L218): @@ -354,7 +347,7 @@ The `EmbeddingProvider` interface is the same shape for every backend. ## Cost telemetry is cross-cutting -Every provider adapter — OpenAI, Anthropic, Ollama, Google, Groq — calls +Every provider adapter, OpenAI, Anthropic, Ollama, Google, Groq, calls `writeCostEvent()` with the same shape after each request. That gives you one cost log across heterogeneous backends, keyed by provider, model, and stage. You can swap models and still see apples-to-apples cost data in a @@ -408,9 +401,13 @@ sidestep all of that by making provider selection fixed for a runtime. Benchmark harnesses should create a fresh isolated runtime or process for each embedding configuration. +## Naming + +This page is about **embedding and LLM providers** inside the engine, OpenAI, Ollama, Anthropic, etc. The SDK has a separate concept called **memory providers** ([`MemoryProvider`](/sdk/concepts/provider-model)), the interface a memory backend implements so the SDK can route through it. Different layer, different concept. + ## Related -- [Stores](./stores.md) — the other half of the platform layer: pluggable +- [Stores](./stores.md), the other half of the platform layer: pluggable storage behind narrow interfaces. -- [Composition](./composition.md) — how providers, stores, and services +- [Composition](./composition.md), how providers, stores, and services are wired together at startup. diff --git a/docs/platform/scope.md b/docs/platform/scope.md index 12b428b..cc80b77 100644 --- a/docs/platform/scope.md +++ b/docs/platform/scope.md @@ -10,18 +10,11 @@ union that dispatches every search, expand, get, list, and delete between a world (multiple agents collaborating over shared and private memories, with SQL-enforced visibility). -> **See also.** The SDK's [Scopes and identity](/sdk/concepts/scopes-and-identity) page covers the client-side `Scope` type and how `UserAccountsManager` resolves identity in browser / server contexts. This page is the canonical reference for the HTTP-level scope semantics below the SDK. - The scope is the security boundary. Routes parse it, the service layer dispatches on it, and the repository layer enforces it in SQL. There is no path that skips it. -{/* - Differentiator note: competing systems either bolt workspace/agent isolation - on as a feature flag (mem0), couple it to a specific agent framework (letta), - or hide it behind a proprietary tenant model (zep). AtomicMemory exposes it - as a typed contract at every seam: HTTP, service, repository. -*/} +The SDK's [Scopes and identity](/sdk/concepts/scopes-and-identity) page covers the client-side `Scope` type and how `UserAccountsManager` resolves identity in browser / server contexts. This page is the canonical reference for the HTTP-level scope semantics below the SDK. ## The contract @@ -41,7 +34,7 @@ Three properties of this contract matter: account. The workspace is a collaboration boundary inside the user's tenancy, not a replacement for it. 2. **`agentId` is required on workspace reads.** There is no "anonymous - workspace read" — the caller always declares which agent is asking, so the + workspace read", the caller always declares which agent is asking, so the visibility SQL has something to compare against. 3. **`agentScope` is separate from `agentId`.** `agentId` answers "who is asking"; `agentScope` answers "whose memories do I want back." They are @@ -100,7 +93,7 @@ async scopedList(scope: MemoryScope, limit: number = 20, offset: number = 0, sou The pattern is deliberate: routes construct a scope once, hand it to the service, and the service picks the right repository call. The legacy non-scoped methods (`search`, `list`, `get`, `delete`) still exist and are -marked `@deprecated` — they remain only so existing callers keep compiling +marked `@deprecated`, they remain only so existing callers keep compiling while they migrate. ## HTTP: building the scope at the edge @@ -122,7 +115,7 @@ function toMemoryScope( The rule: if `workspace_id` is absent from the request, you get a `user`-scoped call. If it is present, the route **must** have also received -an `agent_id` — or the request is rejected before it ever reaches the +an `agent_id`, or the request is rejected before it ever reaches the service layer. ## The `agent_id` requirement (a security fix disguised as an API change) @@ -175,13 +168,13 @@ HTTP/1.1 200 OK { "memories": [...], "count": 7 } ``` -The same check appears in the `GET /:id` and `DELETE /:id` handlers — +The same check appears in the `GET /:id` and `DELETE /:id` handlers , `workspaceId && !agentId` throws an `InputError` before any service call. {/* This is a hard compatibility break with any caller that was relying on the old permissive behavior. It's documented here, in the API reference, and in - the changelog. There is no compatibility shim — the whole point is that + the changelog. There is no compatibility shim, the whole point is that the old behavior was unsafe. */} @@ -189,7 +182,7 @@ The same check appears in the `GET /:id` and `DELETE /:id` handlers — Once `agent_id` reaches the repository layer, a single SQL clause decides what the caller can see. Visibility is not filtered in TypeScript after the -rows come back — it is part of the `WHERE` clause that produces the rows in +rows come back, it is part of the `WHERE` clause that produces the rows in the first place. A memory the caller can't see is invisible to every code path, not just the ones that remembered to check. @@ -234,7 +227,7 @@ Read top-to-bottom, the rules are: A few things worth calling out about this clause: -- It parameterizes `callerAgentId` once and reuses the placeholder — there's +- It parameterizes `callerAgentId` once and reuses the placeholder, there's no string interpolation of agent IDs anywhere in the SQL path. - It composes with the `AgentScope` clause (built by `buildAgentScopeClause` in the same file). Scope narrows *which agents' memories* you want; @@ -263,7 +256,7 @@ type AgentScope = 'all' | 'self' | 'others' | string | string[]; You can pass `agent_scope: 'others'` when the calling agent wants to look up what its teammates have discovered without re-finding its own notes. Or `agent_scope: ['a-planner', 'a-researcher']` to constrain a search to a -specific sub-team. Visibility still runs on top — you cannot see an +specific sub-team. Visibility still runs on top, you cannot see an `agent_only` memory just because you listed its owner in `agent_scope`. ## Designing with scope diff --git a/docs/platform/stores.md b/docs/platform/stores.md index c08738c..2c0e9e8 100644 --- a/docs/platform/stores.md +++ b/docs/platform/stores.md @@ -5,21 +5,16 @@ title: Stores # Stores AtomicMemory's storage layer is exposed as a family of **narrow, domain-facing -store interfaces** — not a monolithic "repository" that every service has to +store interfaces**, not a monolithic "repository" that every service has to import. Each consumer sees only the methods it actually uses, and every seam is a plain TypeScript interface you can re-implement against your own backend. That is the point of the platform layer: the engine's business logic depends on `MemoryStore`, `SearchStore`, `ClaimStore`, `EntityStore`, `EpisodeStore`, and -friends — never on a Postgres pool. If you want to run the same services on a +friends, never on a Postgres pool. If you want to run the same services on a different database, a different vector backend, or an in-memory mock for tests, you implement the interfaces. Nothing above the store layer changes. -{/* Competitive angle: mem0 ships a SaaS+Python stack; letta bakes storage into - the agent framework; zep is a Go commercial product. AtomicMemory is - TypeScript-native with explicit, Pick-narrowed contracts you can - implement piece by piece. */} - ## The bundled shape The composition root hands every service a single `CoreStores` bundle. That @@ -58,7 +53,7 @@ Two things to notice: ## The core interfaces -### `MemoryStore` — memory CRUD and workspace variants +### `MemoryStore`, memory CRUD and workspace variants `MemoryStore` is the canonical write+read surface for individual memories. It includes per-user CRUD, bulk operations, statistics, and parallel @@ -97,7 +92,7 @@ export interface MemoryStore { } ``` -### `SearchStore` — vector, hybrid, keyword, and dedup +### `SearchStore`, vector, hybrid, keyword, and dedup `SearchStore` is the retrieval surface. Vector search, hybrid search, keyword search, atomic-fact search, near-duplicate detection, temporal neighbors, @@ -141,17 +136,17 @@ depend on the write surface. ### Supporting stores -- **`EpisodeStore`** — session/episode lifecycle (`storeEpisode`, `getEpisode`). -- **`SemanticLinkStore`** — create links between memories, find link candidates, +- **`EpisodeStore`**, session/episode lifecycle (`storeEpisode`, `getEpisode`). +- **`SemanticLinkStore`**, create links between memories, find link candidates, traverse link neighborhoods. -- **`RepresentationStore`** — atomic facts + foresight projections derived +- **`RepresentationStore`**, atomic facts + foresight projections derived from raw memories. --- ## The `Pick<>`-narrowing pattern -Three of the stores — `ClaimStore`, `EntityStore`, `LessonStore` — aren't +Three of the stores, `ClaimStore`, `EntityStore`, `LessonStore`, aren't hand-written interfaces. They're **type-level projections** of the underlying repository class, narrowed to exactly the methods domain consumers call. @@ -200,7 +195,7 @@ export type EntityStore = Pick` solves. @@ -211,7 +206,7 @@ pipeline actually calls, we get three things for free: 1. **A contract, not a class.** Consumers depend on a structural type. They can't accidentally reach into implementation internals, and an alternative backend (SQLite, DuckDB, an HTTP adapter, a test mock) only has to - implement those seventeen methods — not the whole repository. + implement those seventeen methods, not the whole repository. 2. **Refactors are safe in one direction.** Adding a new method to `ClaimRepository` can never break consumers of `ClaimStore`. The narrowed surface is invariant to repository growth. @@ -253,7 +248,7 @@ export class PgMemoryStore implements MemoryStore { } ``` -`PgSearchStore` follows the same shape — the constructor takes a pool, and +`PgSearchStore` follows the same shape, the constructor takes a pool, and each method forwards to a pure function in the read/links modules. See [`pg-search-store.ts:23-65`](https://github.com/atomicmemory/atomicmemory-core/blob/main/src/db/pg-search-store.ts#L23-L65). @@ -299,7 +294,7 @@ const stores: CoreStores = { Two details worth calling out: -- `claims`, `entities`, and `lessons` are assigned directly — the repository +- `claims`, `entities`, and `lessons` are assigned directly, the repository classes satisfy the narrowed store types **because the types were built from them**. No adapter class needed. - Feature flags (`entityGraphEnabled`, `lessonsEnabled`) decide whether the @@ -327,7 +322,7 @@ That's what we mean by "pluggable at every seam". ## Related -- [Providers](./providers.md) — the pluggable embedding + LLM layer that +- [Providers](./providers.md), the pluggable embedding + LLM layer that sits alongside stores. -- [Composition](./composition.md) — how the runtime container wires stores, +- [Composition](./composition.md), how the runtime container wires stores, providers, services, and routes. diff --git a/docs/quickstart.md b/docs/quickstart.md index 53465eb..c729d67 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -4,18 +4,18 @@ sidebar_position: 2 # Quickstart -Docker up, first ingest, first search — in under 2 minutes. +Docker up, first ingest, first search, in under 2 minutes. ## Prerequisites - **Docker** (Docker Desktop or Docker Engine with `docker compose`) - **An LLM / embedding provider**, either: - An **OpenAI API key** (default path, zero extra setup), or - - A local **Ollama** install if you prefer to run everything on your machine — see [providers](/platform/providers) for the env vars to swap in + - A local **Ollama** install if you prefer to run everything on your machine, see [providers](/platform/providers) for the env vars to swap in That is all. The Docker stack brings its own Postgres with pgvector. -## Step 1 — Clone and start +## Step 1, Clone and start Clone the core repo, copy the sample env file, drop in your API key, and bring the stack up. @@ -23,13 +23,13 @@ Clone the core repo, copy the sample env file, drop in your API key, and bring t git clone https://github.com/atomicmemory/atomicmemory-core.git cd atomicmemory-core cp .env.example .env -# edit .env — set OPENAI_API_KEY +# edit .env, set OPENAI_API_KEY docker compose up -d --build ``` The `-d` runs detached; `--build` compiles the image on first run. Migrations execute automatically on container start, so the database is ready when the server binds to port `3050`. -## Step 2 — Verify health +## Step 2, Verify health Hit the memory subsystem health endpoint to confirm the server is live and see the active runtime config. @@ -39,7 +39,7 @@ curl http://localhost:3050/v1/memories/health You should get back a JSON object with `"status": "ok"` and a `config` snapshot showing the selected embedding and LLM providers. -## Step 3 — First ingest +## Step 3, First ingest Send a conversation and let AtomicMemory extract structured facts, embed them, and run AUDN to decide what to store. @@ -49,9 +49,9 @@ curl -X POST http://localhost:3050/v1/memories/ingest \ -d '{"user_id":"alice","conversation":"user: I ship Go backends and TypeScript frontends.","source_site":"quickstart"}' ``` -The response reports `facts_extracted`, `memories_stored`, `stored_memory_ids`, and `updated_memory_ids` — that is AUDN telling you what survived dedup and contradiction checks. +The response reports `facts_extracted`, `memories_stored`, `stored_memory_ids`, and `updated_memory_ids`, that is AUDN telling you what survived dedup and contradiction checks. -## Step 4 — First search +## Step 4, First search Query the memories you just created. AtomicMemory runs semantic retrieval, applies the active retrieval profile, and packages the result. Hybrid retrieval (vector + BM25/FTS with RRF fusion) is available through the `quality` profile or per-request config overrides, but the default `balanced` profile keeps it off. @@ -63,20 +63,20 @@ curl -X POST http://localhost:3050/v1/memories/search \ The response carries three things worth noting: -- **`memories`** — ranked results with `similarity`, composite `score`, and `importance` -- **`injection_text`** — a pre-formatted markdown block, date-stamped and grouped by source, ready to paste directly into an LLM system prompt -- **`observability`** — stable trace metadata (timing, retrieval mode, citations) so you can wire evals and dashboards without reverse-engineering internals +- **`memories`**, ranked results with `similarity`, composite `score`, and `importance` +- **`injection_text`**, a pre-formatted markdown block, date-stamped and grouped by source, ready to paste directly into an LLM system prompt +- **`observability`**, stable trace metadata (timing, retrieval mode, citations) so you can wire evals and dashboards without reverse-engineering internals ## What next -- **Swap to a local embedding model** — run entirely offline with `transformers` (WASM) or `ollama`. See [providers](/platform/providers). -- **Workspace-scoped memory** — separate personal memory from team memory with first-class scope dispatch. See [scope](/platform/scope). -- **Full API reference** — every endpoint, request, and response shape. Start with [ingest](/api-reference/http/ingest-memory). +- **Swap to a local embedding model**, run entirely offline with `transformers` (WASM) or `ollama`. See [providers](/platform/providers). +- **Workspace-scoped memory**, separate personal memory from team memory with first-class scope dispatch. See [scope](/platform/scope). +- **Full API reference**, every endpoint, request, and response shape. Start with [ingest](/api-reference/http/ingest-memory). ## Using the TypeScript SDK -If you're building in TypeScript or JavaScript, the [SDK Quickstart](/sdk/quickstart) shows the same ingest-and-search round trip above, typed, with the SDK's policy layer (capture / injection gates) in front of the same core endpoints. The SDK is not required — everything above is just HTTP — but it handles the ergonomics, the scope helpers, and the backend-agnostic routing if you want to target more than one memory engine from the same code. +If you're building in TypeScript or JavaScript, the [SDK Quickstart](/sdk/quickstart) shows the same ingest-and-search round trip above, typed, with the SDK's policy layer (capture / injection gates) in front of the same core endpoints. The SDK is not required, everything above is just HTTP, but it handles the ergonomics, the scope helpers, and the backend-agnostic routing if you want to target more than one memory engine from the same code. ## Running in production -The Docker Compose stack above is the same shape you would deploy to a server or a managed container platform. For hardened production guidance — managed Postgres, secret handling, CORS, and the `CORE_RUNTIME_CONFIG_MUTATION_ENABLED` gate — follow the deployment section of the [core repo README](https://github.com/atomicmemory/atomicmemory-core#deployment). +The Docker Compose stack above is the same shape you would deploy to a server or a managed container platform. For hardened production guidance, managed Postgres, secret handling, CORS, and the `CORE_RUNTIME_CONFIG_MUTATION_ENABLED` gate, follow the deployment section of the [core repo README](https://github.com/atomicmemory/atomicmemory-core#deployment). diff --git a/docs/sdk/api/errors.md b/docs/sdk/api/errors.md index 6747eaf..339898a 100644 --- a/docs/sdk/api/errors.md +++ b/docs/sdk/api/errors.md @@ -11,7 +11,7 @@ Every error the SDK throws descends from `AtomicMemoryError`. Catching that one | Class | Thrown when | Retryable? | Typical handling | |---|---|---|---| -| `AtomicMemoryError` | Base class; abstract in practice | — | Catch for "anything from the SDK"; inspect subclass to branch | +| `AtomicMemoryError` | Base class; abstract in practice | n/a | Catch for "anything from the SDK"; inspect subclass to branch | | `ConfigurationError` | Invalid or missing config at construction / init | No | Fix config; do not retry | | `NetworkError` | HTTP failure, timeout, DNS failure talking to a backend | Yes (bounded) | Retry with backoff; surface a user-friendly error after N attempts | | `StorageError` | Adapter I/O failure (IndexedDB quota, transaction abort, corruption) | Partially | Retry for transient; escalate for persistent (the resilience layer does this automatically) | @@ -61,7 +61,7 @@ try { ## Retry policy -The SDK's storage and network layers include a `RetryEngine` that handles transient failures automatically (exponential backoff, bounded attempts). You do not typically retry at the application level on top of that — doubling up retries tends to compound latency without improving success rate. +The SDK's storage and network layers include a `RetryEngine` that handles transient failures automatically (exponential backoff, bounded attempts). You do not typically retry at the application level on top of that, doubling up retries tends to compound latency without improving success rate. If you need custom retry behaviour, the `RetryPolicy` type is exported from the error-handling module; construct your own `RetryableOperation` around a bare provider call. @@ -71,5 +71,5 @@ Every `AtomicMemoryError` subclass carries a `name` (for logging / telemetry) an ## Next -- [MemoryProvider contract](/sdk/api/memory-provider) — which errors your provider implementation should throw -- [Capabilities](/sdk/concepts/capabilities) — the guard that prevents `UnsupportedOperationError` +- [MemoryProvider contract](/sdk/api/memory-provider), which errors your provider implementation should throw +- [Capabilities](/sdk/concepts/capabilities), the guard that prevents `UnsupportedOperationError` diff --git a/docs/sdk/api/memory-provider.md b/docs/sdk/api/memory-provider.md index 29a8aab..ece7d6e 100644 --- a/docs/sdk/api/memory-provider.md +++ b/docs/sdk/api/memory-provider.md @@ -16,10 +16,10 @@ Every provider must implement these methods. No exceptions. | `name: string` | Stable identifier used for registry lookup and capability reports. Match the key used in `MemoryClient`'s `providers` config. | | `ingest(input)` | Accept one of the declared `ingestModes` (`text`, `messages`, `memory`). Return `IngestResult` describing what was created / updated / unchanged. | | `search(request)` | Return a `SearchResultPage`. Honour `limit`, `cursor`, `scope`. Scope-invisible data must not surface. | -| `get(ref)` | Return the memory or `null` — never throw for "not found". | +| `get(ref)` | Return the memory or `null`, never throw for "not found". | | `delete(ref)` | Idempotent. Deleting a missing memory is not an error. | | `list(request)` | Paginate via `cursor`. Respect `scope` and `filter`. | -| `capabilities()` | Return the full `Capabilities` object — honestly (see below). | +| `capabilities()` | Return the full `Capabilities` object, honestly (see below). | Extending `BaseMemoryProvider` is the supported path. It provides scope-validation scaffolding and a default `getExtension` implementation that relies on your declared capabilities. @@ -27,7 +27,7 @@ Extending `BaseMemoryProvider` is the supported path. It provides scope-validati `capabilities()` is a declaration, not a hope. Apps read it and branch. Misrepresenting capabilities breaks callers in ways that are hard to debug. -- If `extensions.package` is `true`, `getExtension('package')` must return a real `Packager`. The default `BaseMemoryProvider.getExtension` returns `this` when the capability is true — so the subclass must implement `package(request)` on itself. +- If `extensions.package` is `true`, `getExtension('package')` must return a real `Packager`. The default `BaseMemoryProvider.getExtension` returns `this` when the capability is true, so the subclass must implement `package(request)` on itself. - `requiredScope` is `{ default: Array, ingest?: ..., search?: ..., ... }`. If `'user'` appears in `default` (or any per-operation override), every request for that operation missing `scope.user` should be rejected before any network call. `BaseMemoryProvider.validateScope` does this for you when called. - `ingestModes` lists the shapes you accept. Rejecting an unlisted mode is fine; accepting one outside the list is not. @@ -40,7 +40,7 @@ The default `getExtension(name)` on `BaseMemoryProvider`: 3. Otherwise checks `capabilities().customExtensions` and returns the registered object. 4. Otherwise returns `undefined`. -Subclasses override `getExtension` only when they need to return a different object for a standard extension — rare. The typical path is: declare the capability, implement the methods on your subclass, inherit the default resolution. +Subclasses override `getExtension` only when they need to return a different object for a standard extension, rare. The typical path is: declare the capability, implement the methods on your subclass, inherit the default resolution. ## Extension interfaces (reference list) @@ -57,18 +57,18 @@ Subclasses override `getExtension` only when they need to return a different obj | `batch` | `BatchOps` | `batchIngest(inputs)` and similar bulk APIs | | `health` | `Health` | `health()` returning a liveness status | -If you want to offer a capability not on this list, use `customExtensions` — the registry passes arbitrary objects through `getExtension(name)`. Apps that use custom extensions cast the return value to the expected type. +If you want to offer a capability not on this list, use `customExtensions`, the registry passes arbitrary objects through `getExtension(name)`. Apps that use custom extensions cast the return value to the expected type. ## Lifecycle -- **`initialize?()`** — optional. Runs once after construction. The right place for health checks, warm-up, long-lived connections. -- **`close?()`** — optional. Release connections, flush caches. Callers invoke it directly on the provider when they need teardown; `MemoryClient` itself does not drive it. +- **`initialize?()`**, optional. Runs once after construction. The right place for health checks, warm-up, long-lived connections. +- **`close?()`**, optional. Release connections, flush caches. Callers invoke it directly on the provider when they need teardown; `MemoryClient` itself does not drive it. Both are async. Both may throw; `initialize` errors propagate to the caller of `MemoryClient.initialize()`. ## Error expectations -Throw errors from the SDK's error hierarchy when you can — `NetworkError` for wire failures, `ConfigurationError` for bad config, `UnsupportedOperationError` when a caller requests something you don't implement. See [Errors](/sdk/api/errors). If you throw something else, wrap it at the boundary; do not let arbitrary errors leak through `MemoryService` — callers rely on the hierarchy for programmatic handling. +Throw errors from the SDK's error hierarchy when you can, `NetworkError` for wire failures, `ConfigurationError` for bad config, `UnsupportedOperationError` when a caller requests something you don't implement. See [Errors](/sdk/api/errors). If you throw something else, wrap it at the boundary; do not let arbitrary errors leak through `MemoryService`, callers rely on the hierarchy for programmatic handling. ## Minimal example @@ -76,5 +76,5 @@ See [Writing a custom provider](/sdk/guides/custom-provider) for the end-to-end ## Next -- [Errors](/sdk/api/errors) — the error hierarchy your provider should throw into -- [Capabilities](/sdk/concepts/capabilities) — the runtime contract apps rely on when they consume your provider +- [Errors](/sdk/api/errors), the error hierarchy your provider should throw into +- [Capabilities](/sdk/concepts/capabilities), the runtime contract apps rely on when they consume your provider diff --git a/docs/sdk/api/overview.md b/docs/sdk/api/overview.md index 71e0710..0177a40 100644 --- a/docs/sdk/api/overview.md +++ b/docs/sdk/api/overview.md @@ -11,20 +11,20 @@ The SDK's reference documentation comes in two shapes: **hand-written** contract Three pages, each covering something human writing adds that generated docs cannot: -- **[MemoryProvider contract](/sdk/api/memory-provider)** — the interface you implement when [writing a custom provider](/sdk/guides/custom-provider). This is an authoring contract, not a reference listing — the focus is on obligations (required methods, `capabilities()` rules, extension-resolution semantics), not signatures. -- **[Errors](/sdk/api/errors)** — the `AtomicMemoryError` hierarchy with a handling cheatsheet. Small, stable, and worth reading linearly. +- **[MemoryProvider contract](/sdk/api/memory-provider)**, the interface you implement when [writing a custom provider](/sdk/guides/custom-provider). This is an authoring contract, not a reference listing, the focus is on obligations (required methods, `capabilities()` rules, extension-resolution semantics), not signatures. +- **[Errors](/sdk/api/errors)**, the `AtomicMemoryError` hierarchy with a handling cheatsheet. Small, stable, and worth reading linearly. ## What's generated -Method signatures, class members, types, and subpath exports — `MemoryClient`, `IngestInput`, `SearchRequest`, `ContextPackage`, `StorageManager`, `EmbeddingGenerator`, and the rest — are generated from the SDK's source via its `typedoc` pipeline. +Method signatures, class members, types, and subpath exports, `MemoryClient`, `IngestInput`, `SearchRequest`, `ContextPackage`, `StorageManager`, `EmbeddingGenerator`, and the rest, are generated from the SDK's source via its `typedoc` pipeline. Until the generated reference is published alongside these docs, point users at the SDK repo's types and JSDoc comments directly: -- `src/client/memory-client.ts` — `MemoryClient` class -- `src/memory/types.ts` — public types (`Scope`, `MemoryRef`, `Memory`, `IngestInput`, `SearchRequest`, `SearchResultPage`, `PackageRequest`, `ContextPackage`, `Capabilities`) -- `src/memory/provider.ts` — `MemoryProvider` + `BaseMemoryProvider` + extension interfaces -- `src/storage/`, `src/embedding/`, `src/search/` — subpath exports -- `src/browser.ts` — the slim browser entry +- `src/client/memory-client.ts`, `MemoryClient` class +- `src/memory/types.ts`, public types (`Scope`, `MemoryRef`, `Memory`, `IngestInput`, `SearchRequest`, `SearchResultPage`, `PackageRequest`, `ContextPackage`, `Capabilities`) +- `src/memory/provider.ts`, `MemoryProvider` + `BaseMemoryProvider` + extension interfaces +- `src/storage/`, `src/embedding/`, `src/search/`, subpath exports +- `src/browser.ts`, the slim browser entry ## Why this split @@ -32,4 +32,4 @@ The SDK's TypeScript types are the source of truth for method signatures. Writin ## Linking from concept pages -Every concept page that mentions a method or type is meant to link into the generated reference. Until that lands, those links point to source files in the SDK repository. The contract pages under this section remain valid either way — they're not describing the types, they're describing the agreements you make with the SDK when you consume or extend it. +Every concept page that mentions a method or type is meant to link into the generated reference. Until that lands, those links point to source files in the SDK repository. The contract pages under this section remain valid either way, they're not describing the types, they're describing the agreements you make with the SDK when you consume or extend it. diff --git a/docs/sdk/concepts/architecture.md b/docs/sdk/concepts/architecture.md index 20ec076..9c6df36 100644 --- a/docs/sdk/concepts/architecture.md +++ b/docs/sdk/concepts/architecture.md @@ -73,6 +73,6 @@ Search follows the same shape. Every operation on `MemoryClient` delegates to `M ## Where to go next -- [Provider model](/sdk/concepts/provider-model) — the `MemoryProvider` interface and the extension system -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the `Scope` shape every operation carries -- [Capabilities](/sdk/concepts/capabilities) — how to probe what a provider supports +- [Provider model](/sdk/concepts/provider-model), the `MemoryProvider` interface and the extension system +- [Scopes and identity](/sdk/concepts/scopes-and-identity), the `Scope` shape every operation carries +- [Capabilities](/sdk/concepts/capabilities), how to probe what a provider supports diff --git a/docs/sdk/concepts/capabilities.md b/docs/sdk/concepts/capabilities.md index 93acf49..ee431d7 100644 --- a/docs/sdk/concepts/capabilities.md +++ b/docs/sdk/concepts/capabilities.md @@ -23,9 +23,9 @@ const caps = memory.capabilities(); Three fields matter in practice: -- **`ingestModes`** — which shapes of input the provider accepts. Most providers accept all three; some specialize. -- **`requiredScope`** — which scope fields must be present in a request. A `default` array lists the fields required across all operations, with optional per-operation overrides keyed by operation name (`ingest`, `search`, `get`, `delete`, `list`, `package`, `update`, ...). A workspace-only provider might require `namespace` in `default`; a personal-memory provider might require only `user`. -- **`extensions`** — a record of capability keys to booleans. `package`, `temporal`, `versioning`, `update`, `graph`, `forget`, `profile`, `reflect`, `batch`, `health`. If `extensions.package` is `true`, the provider supports `memory.package()`. If `false`, calling `memory.package()` raises `UnsupportedOperationError`. +- **`ingestModes`**, which shapes of input the provider accepts. Most providers accept all three; some specialize. +- **`requiredScope`**, which scope fields must be present in a request. A `default` array lists the fields required across all operations, with optional per-operation overrides keyed by operation name (`ingest`, `search`, `get`, `delete`, `list`, `package`, `update`, ...). A workspace-only provider might require `namespace` in `default`; a personal-memory provider might require only `user`. +- **`extensions`**, a record of capability keys to booleans. `package`, `temporal`, `versioning`, `update`, `graph`, `forget`, `profile`, `reflect`, `batch`, `health`. If `extensions.package` is `true`, the provider supports `memory.package()`. If `false`, calling `memory.package()` raises `UnsupportedOperationError`. ## The extension probe @@ -41,7 +41,7 @@ if (packager) { } ``` -The extension key matches the capability key (`'package'`, not `'packager'`) — this is the contract, and it matches the implementation (`src/memory/memory-service.ts:188`). +The extension key matches the capability key (`'package'`, not `'packager'`), this is the contract, and it matches the implementation (`src/memory/memory-service.ts:188`). ## Why apps must check @@ -64,9 +64,9 @@ if (extensions.package) { ## Capabilities are read once -`capabilities()` is not a runtime guard — it is a declaration. Providers do not change their capabilities after initialization, so reading the object once at startup is fine. Cache it if you check frequently. +`capabilities()` is not a runtime guard, it is a declaration. Providers do not change their capabilities after initialization, so reading the object once at startup is fine. Cache it if you check frequently. ## Next -- [Provider model](/sdk/concepts/provider-model) — the interface the capabilities describe -- [Swapping backends](/sdk/guides/swapping-backends) — the operational story when capabilities differ across providers +- [Provider model](/sdk/concepts/provider-model), the interface the capabilities describe +- [Swapping backends](/sdk/guides/swapping-backends), the operational story when capabilities differ across providers diff --git a/docs/sdk/concepts/embeddings.md b/docs/sdk/concepts/embeddings.md index 5f3d55a..59139b0 100644 --- a/docs/sdk/concepts/embeddings.md +++ b/docs/sdk/concepts/embeddings.md @@ -5,7 +5,7 @@ sidebar_position: 6 # Embeddings -The SDK ships **local** embedding generation through `@huggingface/transformers`. Applications can embed text in the browser, in a worker, or in Node — no API calls, no round trip, no per-token cost. +The SDK ships **local** embedding generation through `@huggingface/transformers`. Applications can embed text in the browser, in a worker, or in Node, no API calls, no round trip, no per-token cost. This is the `/embedding` subpath export. It's independent of `MemoryClient`; the top-level client does not use the embedding module directly (backends embed server-side). The `EmbeddingGenerator` exists for apps that want client-side semantic search over their own data. @@ -28,7 +28,7 @@ Text goes in, vectors come out, vectors get compared, ranked results come back. ## Default model -`Xenova/all-MiniLM-L6-v2`, 384 dimensions, int8-quantized. This matches what `atomicmemory-core` uses when configured with the `transformers` embedding provider — so the same embeddings are comparable across client and server if you ever need to mix them. +`Xenova/all-MiniLM-L6-v2`, 384 dimensions, int8-quantized. This matches what `atomicmemory-core` uses when configured with the `transformers` embedding provider, so the same embeddings are comparable across client and server if you ever need to mix them. You can override the model: @@ -49,7 +49,7 @@ const result = await generator.generateEmbedding('Hello world'); First-call latency for a new model is dominated by download (~20 MB for the default) and compile. Subsequent calls in the same session are milliseconds. Across sessions, the ONNX weights are cached in IndexedDB by `transformers.js` itself; the SDK does not re-download. -The `cache-safety` helpers in `src/embedding/` guard against corrupted cache entries — they validate, repair, or re-download as needed. +The `cache-safety` helpers in `src/embedding/` guard against corrupted cache entries, they validate, repair, or re-download as needed. ## WASM loading @@ -62,9 +62,9 @@ The default transformers.js build loads its WASM runtime from the CDN. For offli - Offline demos - Combining embeddings from the same model as the server for consistency -If you're using `MemoryClient` with `atomicmemory-core` or Mem0, you probably do **not** need this module — the backend embeds for you. +If you're using `MemoryClient` with `atomicmemory-core` or Mem0, you probably do **not** need this module, the backend embeds for you. ## Next -- [Semantic search primitives](/sdk/guides/browser-primitives) — using `EmbeddingGenerator` with `SemanticSearch` for a full client-side pipeline -- [Storage adapters](/sdk/concepts/storage-adapters) — where the model cache and any stored vectors live +- [Semantic search primitives](/sdk/guides/browser-primitives), using `EmbeddingGenerator` with `SemanticSearch` for a full client-side pipeline +- [Storage adapters](/sdk/concepts/storage-adapters), where the model cache and any stored vectors live diff --git a/docs/sdk/concepts/provider-model.md b/docs/sdk/concepts/provider-model.md index f3c0b28..d4fe4e5 100644 --- a/docs/sdk/concepts/provider-model.md +++ b/docs/sdk/concepts/provider-model.md @@ -5,8 +5,6 @@ sidebar_position: 2 # Memory providers -> **Disambiguation.** On this page, "provider" means a **memory backend** — a concrete implementation of the `MemoryProvider` interface that `MemoryClient` routes operations through. Core's [providers](/platform/providers) page is about **embedding and LLM providers** inside the engine (OpenAI, Ollama, etc.). Different layer, different concept. - The provider model is what makes `MemoryClient` backend-agnostic. It has three pieces: an interface every backend implements, a registry the client consults at init time, and an extension system for backend-specific capabilities. ## The interface @@ -49,7 +47,7 @@ classDiagram Mem0Provider ..|> Health ``` -Every backend implements the same six core operations: `ingest`, `search`, `get`, `delete`, `list`, `capabilities`. Beyond the core, the provider may opt into any of a fixed menu of **extensions** — `Packager`, `TemporalSearch`, `Versioner`, `Updater`, `GraphSearch`, `Forgetter`, `Profiler`, `Reflector`, `BatchOps`, `Health` — and declare which ones it supports through its `Capabilities` object. +Every backend implements the same six core operations: `ingest`, `search`, `get`, `delete`, `list`, `capabilities`. Beyond the core, the provider may opt into any of a fixed menu of **extensions**, `Packager`, `TemporalSearch`, `Versioner`, `Updater`, `GraphSearch`, `Forgetter`, `Profiler`, `Reflector`, `BatchOps`, `Health`, and declare which ones it supports through its `Capabilities` object. This is why capabilities are runtime-queryable: an app that wants to use `memory.package()` must first check that the active provider supports the `package` extension, because not every backend does. @@ -67,7 +65,7 @@ new MemoryClient({ }); ``` -`defaultProvider` names the provider every operation routes to unless overridden. When only one provider is configured, it is the default implicitly. A custom provider is registered the same way — see [Writing a custom provider](/sdk/guides/custom-provider). +`defaultProvider` names the provider every operation routes to unless overridden. When only one provider is configured, it is the default implicitly. A custom provider is registered the same way, see [Writing a custom provider](/sdk/guides/custom-provider). ## Extensions: the probe pattern @@ -103,12 +101,16 @@ This design is deliberate: extensions are optional, but they are not second-clas ## Shipped providers -- **`AtomicMemoryProvider`** — HTTP client for `atomicmemory-core`. Implements the core operations plus `Packager`, `TemporalSearch`, `Versioner`, `Updater`, `Health`. See [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend). -- **`Mem0Provider`** — HTTP client for Mem0. Implements core operations plus `Health`. See [Using the Mem0 backend](/sdk/guides/mem0-backend). +- **`AtomicMemoryProvider`**, HTTP client for `atomicmemory-core`. Implements the core operations plus `Packager`, `TemporalSearch`, `Versioner`, `Updater`, `Health`. See [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend). +- **`Mem0Provider`**, HTTP client for Mem0. Implements core operations plus `Health`. See [Using the Mem0 backend](/sdk/guides/mem0-backend). + +A third provider is always possible, you write it. See [Writing a custom provider](/sdk/guides/custom-provider). + +## Naming -A third provider is always possible — you write it. See [Writing a custom provider](/sdk/guides/custom-provider). +On this page "provider" means a **memory backend**, a concrete implementation of the `MemoryProvider` interface that `MemoryClient` routes operations through. Core's [providers](/platform/providers) page is about **embedding and LLM providers** inside the engine (OpenAI, Ollama, etc.). Different layer, different concept. ## Next -- [Capabilities](/sdk/concepts/capabilities) — how to query what a provider supports before calling an extension -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the scope model every provider receives +- [Capabilities](/sdk/concepts/capabilities), how to query what a provider supports before calling an extension +- [Scopes and identity](/sdk/concepts/scopes-and-identity), the scope model every provider receives diff --git a/docs/sdk/concepts/scopes-and-identity.md b/docs/sdk/concepts/scopes-and-identity.md index 32011d5..7343048 100644 --- a/docs/sdk/concepts/scopes-and-identity.md +++ b/docs/sdk/concepts/scopes-and-identity.md @@ -5,7 +5,7 @@ sidebar_position: 4 # Scopes and identity -Every SDK operation carries a **scope** — the partition the memory lives in. Scope is what separates Alice's memory from Bob's, the personal workspace from the team workspace, one agent's lessons from another. +Every SDK operation carries a **scope**, the partition the memory lives in. Scope is what separates Alice's memory from Bob's, the personal workspace from the team workspace, one agent's lessons from another. > Core is canonical for the HTTP-level scope semantics. See [platform/scope](/platform/scope) for the wire-format view. This page covers what the SDK presents on the client side. @@ -37,7 +37,7 @@ await memory.ingest({ }); ``` -Where `currentSession.userId` comes from — an auth service, a JWT, a hard-coded identifier for a CLI tool — is an application concern. The SDK treats the string as opaque and passes it to the active provider. +Where `currentSession.userId` comes from, an auth service, a JWT, a hard-coded identifier for a CLI tool, is an application concern. The SDK treats the string as opaque and passes it to the active provider. ## Scope patterns @@ -48,7 +48,7 @@ Where `currentSession.userId` comes from — an auth service, a JWT, a hard-code | Per-agent memory | `{ user, namespace, agent }` | Multi-agent systems where agents keep their own memories | | Per-conversation | `{ user, thread }` | Scratch memory tied to a single session | -A memory written at `{ user: 'alice', namespace: 'team-acme' }` is not visible to a search at `{ user: 'alice' }` unless the provider opts into upward-narrowing (most do not, on purpose — this is what makes the isolation real). +A memory written at `{ user: 'alice', namespace: 'team-acme' }` is not visible to a search at `{ user: 'alice' }` unless the provider opts into upward-narrowing (most do not, on purpose, this is what makes the isolation real). ## Scope validation @@ -56,5 +56,5 @@ A memory written at `{ user: 'alice', namespace: 'team-acme' }` is not visible t ## Next -- [Provider model](/sdk/concepts/provider-model) — how each provider declares its `requiredScope` -- Core's [platform/scope](/platform/scope) — the HTTP-level scope model and visibility rules +- [Provider model](/sdk/concepts/provider-model), how each provider declares its `requiredScope` +- Core's [platform/scope](/platform/scope), the HTTP-level scope model and visibility rules diff --git a/docs/sdk/concepts/storage-adapters.md b/docs/sdk/concepts/storage-adapters.md index d75adf4..19286a3 100644 --- a/docs/sdk/concepts/storage-adapters.md +++ b/docs/sdk/concepts/storage-adapters.md @@ -5,9 +5,9 @@ sidebar_position: 5 # Storage adapters -The SDK's storage subsystem is a thin, resilient abstraction over key-value storage. It lives behind the `/storage` subpath export and is intended for applications that need persistent client-side state — for example, to pair with the `/embedding` and `/search` primitives in a no-backend topology. +The SDK's storage subsystem is a thin, resilient abstraction over key-value storage. It lives behind the `/storage` subpath export and is intended for applications that need persistent client-side state, for example, to pair with the `/embedding` and `/search` primitives in a no-backend topology. -Storage is a separate concern from the memory **backend**. The backend holds your memories; storage holds local data — caches, application state, queued writes, bundled knowledge bases. `MemoryClient` does not use `StorageManager` internally. +Storage is a separate concern from the memory **backend**. The backend holds your memories; storage holds local data, caches, application state, queued writes, bundled knowledge bases. `MemoryClient` does not use `StorageManager` internally. ## The subsystem @@ -27,9 +27,9 @@ flowchart LR Three parts matter: -- **`StorageManager`** — the facade applications call. Handles validation, delegates to an adapter, runs the resilience stack. -- **`StorageAdapter` interface** — the contract a concrete storage mechanism implements. Six operations: `get`, `set`, `delete`, `clear`, `has`, `keys`, plus `batch` and `stats`. -- **Resilience layer** — retry, circuit breaker, health tracking, and corruption repair. These are on by default and can be tuned per deployment. +- **`StorageManager`**, the facade applications call. Handles validation, delegates to an adapter, runs the resilience stack. +- **`StorageAdapter` interface**, the contract a concrete storage mechanism implements. Six operations: `get`, `set`, `delete`, `clear`, `has`, `keys`, plus `batch` and `stats`. +- **Resilience layer**, retry, circuit breaker, health tracking, and corruption repair. These are on by default and can be tuned per deployment. ## Shipped adapters @@ -58,7 +58,7 @@ const prefs = await storage.get<{ theme: string }>('preferences'); ## Chrome extensions and other targets -The SDK does **not** ship a `ChromeStorageAdapter`. Extension authors who want to back storage with `chrome.storage.local` implement the `StorageAdapter` interface themselves — the interface is small and the path is deliberate. +The SDK does **not** ship a `ChromeStorageAdapter`. Extension authors who want to back storage with `chrome.storage.local` implement the `StorageAdapter` interface themselves, the interface is small and the path is deliberate. ## Writing a custom adapter @@ -103,14 +103,14 @@ Plug it into `StorageManager` like any other adapter. ## Resilience: what is on by default -- **Retry** — transient failures (quota, transaction aborts) are retried with exponential backoff. -- **Circuit breaker** — sustained failures open the circuit; subsequent calls fail fast until the breaker half-opens and tests recovery. -- **Health tracking** — the manager tracks adapter health and emits events on degradation. -- **Repair** — detects corrupted entries in IndexedDB and attempts repair before falling back to an error. +- **Retry**, transient failures (quota, transaction aborts) are retried with exponential backoff. +- **Circuit breaker**, sustained failures open the circuit; subsequent calls fail fast until the breaker half-opens and tests recovery. +- **Health tracking**, the manager tracks adapter health and emits events on degradation. +- **Repair**, detects corrupted entries in IndexedDB and attempts repair before falling back to an error. All of this is opt-out, not opt-in. Applications that want stricter failure semantics can disable the resilience layer in favour of raw adapter calls. ## Next -- [Embeddings](/sdk/concepts/embeddings) — which uses IndexedDB under the hood for its model cache -- [Browser primitives](/sdk/guides/browser-primitives) — composing storage with embeddings and search outside `MemoryClient` +- [Embeddings](/sdk/concepts/embeddings), which uses IndexedDB under the hood for its model cache +- [Browser primitives](/sdk/guides/browser-primitives), composing storage with embeddings and search outside `MemoryClient` diff --git a/docs/sdk/guides/atomicmemory-backend.md b/docs/sdk/guides/atomicmemory-backend.md index a69e8c6..3505dcc 100644 --- a/docs/sdk/guides/atomicmemory-backend.md +++ b/docs/sdk/guides/atomicmemory-backend.md @@ -37,7 +37,7 @@ console.log(status); // [{ name: 'atomicmemory', initialized: true, capabilities: { ... } }] ``` -For a deeper liveness check, hit the core `/v1/memories/health` endpoint directly — it returns the full config snapshot (embedding / LLM provider, thresholds). +For a deeper liveness check, hit the core `/v1/memories/health` endpoint directly, it returns the full config snapshot (embedding / LLM provider, thresholds). ## One ingest, one search @@ -71,22 +71,22 @@ Every SDK method has a corresponding HTTP endpoint on core. Useful if you're deb `AtomicMemoryProvider` declares the following extensions. Apps can rely on all of them: -- `package` — context packaging with token budgets -- `temporal` — `searchAsOf` for point-in-time queries -- `versioning` — `history()` per memory ref -- `update` — in-place updates -- `health` — liveness probe +- `package`, context packaging with token budgets +- `temporal`, `searchAsOf` for point-in-time queries +- `versioning`, `history()` per memory ref +- `update`, in-place updates +- `health`, liveness probe -Run `memory.capabilities()` at init and cache the result — you'll know exactly what's available without calling around. +Run `memory.capabilities()` at init and cache the result, you'll know exactly what's available without calling around. ## Production checklist -- **`apiKey`** — set a bearer token on the SDK side and a matching gate on core. Don't rely on `testMode` identity in production. -- **`timeout`** — the default 30s is fine for most paths. Tighten for latency-sensitive UIs; loosen for batch workloads that trigger large searches. -- **`apiVersion`** — pin to the version your core is on. Leave as `'v1'` unless you've deployed a different API version. -- **Network reachability** — core runs on port `3050` by default. Verify from your deployment target (container? serverless? browser?) that the URL resolves. +- **`apiKey`**, set a bearer token on the SDK side and a matching gate on core. Don't rely on `testMode` identity in production. +- **`timeout`**, the default 30s is fine for most paths. Tighten for latency-sensitive UIs; loosen for batch workloads that trigger large searches. +- **`apiVersion`**, pin to the version your core is on. Leave as `'v1'` unless you've deployed a different API version. +- **Network reachability**, core runs on port `3050` by default. Verify from your deployment target (container? serverless? browser?) that the URL resolves. ## Next -- [Swapping backends](/sdk/guides/swapping-backends) — moving memories from atomicmemory to another provider -- [Using the Mem0 backend](/sdk/guides/mem0-backend) — the alternate path +- [Swapping backends](/sdk/guides/swapping-backends), moving memories from atomicmemory to another provider +- [Using the Mem0 backend](/sdk/guides/mem0-backend), the alternate path diff --git a/docs/sdk/guides/browser-helpers.md b/docs/sdk/guides/browser-helpers.md index 2b19530..43be128 100644 --- a/docs/sdk/guides/browser-helpers.md +++ b/docs/sdk/guides/browser-helpers.md @@ -13,7 +13,7 @@ The SDK ships a slim entry point for applications that run in the browser agains import { MemoryClient } from '@atomicmemory/atomicmemory-sdk/browser'; ``` -The `./browser` entry exports `MemoryClient`, the `MemoryProvider` interface and base class, both shipped provider adapters, and the core types. It intentionally omits `storage`, `embedding`, `search`, and `utils` — three bundles that pull in IndexedDB helpers, the transformers WASM runtime, and their dependencies. If your app talks to a remote backend and has no need for on-device persistence or local embeddings, this is the smaller import. +The `./browser` entry exports `MemoryClient`, the `MemoryProvider` interface and base class, both shipped provider adapters, and the core types. It intentionally omits `storage`, `embedding`, `search`, and `utils`, three bundles that pull in IndexedDB helpers, the transformers WASM runtime, and their dependencies. If your app talks to a remote backend and has no need for on-device persistence or local embeddings, this is the smaller import. ## When to use `./browser` @@ -23,7 +23,7 @@ The `./browser` entry exports `MemoryClient`, the `MemoryProvider` interface and ## When to use the root package -- You want client-side semantic search over bundled data without a backend — see [Browser primitives](/sdk/guides/browser-primitives). +- You want client-side semantic search over bundled data without a backend, see [Browser primitives](/sdk/guides/browser-primitives). - You're composing `StorageManager`, `EmbeddingGenerator`, or `SemanticSearch` directly for custom pipelines. - You want one import surface that covers both the client and the primitives. @@ -31,9 +31,9 @@ Both entries expose the same `MemoryClient`. The root re-exports everything from ## Chrome extension storage -The SDK does **not** ship a `ChromeStorageAdapter`. If you want to back storage with `chrome.storage.local`, write a custom adapter — see [Storage adapters](/sdk/concepts/storage-adapters). The interface is small. +The SDK does **not** ship a `ChromeStorageAdapter`. If you want to back storage with `chrome.storage.local`, write a custom adapter, see [Storage adapters](/sdk/concepts/storage-adapters). The interface is small. ## Next -- [Browser primitives](/sdk/guides/browser-primitives) — composing the subpath exports for on-device semantic search -- [Storage adapters](/sdk/concepts/storage-adapters) — the interface you implement for Chrome-extension storage +- [Browser primitives](/sdk/guides/browser-primitives), composing the subpath exports for on-device semantic search +- [Storage adapters](/sdk/concepts/storage-adapters), the interface you implement for Chrome-extension storage diff --git a/docs/sdk/guides/browser-primitives.md b/docs/sdk/guides/browser-primitives.md index 49aab03..cfb870f 100644 --- a/docs/sdk/guides/browser-primitives.md +++ b/docs/sdk/guides/browser-primitives.md @@ -6,7 +6,7 @@ sidebar_position: 6 # Browser primitives :::note -This guide bypasses `MemoryClient`. You get storage, local embeddings, and semantic search — but no provider, no extensions, no remote sync. If you want a backend-agnostic client, start at [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) instead. +This guide bypasses `MemoryClient`. You get storage, local embeddings, and semantic search, but no provider, no extensions, no remote sync. If you want a backend-agnostic client, start at [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) instead. ::: The `/storage`, `/embedding`, and `/search` subpath exports work on their own. You can build client-side semantic search over your app's data without ever constructing `MemoryClient`, and without a memory backend of any kind. @@ -76,7 +76,7 @@ for (const hit of results.slice(0, 3)) { } ``` -The exact `SemanticSearch` API differs slightly by version — import the types from `@atomicmemory/atomicmemory-sdk/search` and read the shape. The pattern is always: vectorize the query, score it against stored vectors, rank. +The exact `SemanticSearch` API differs slightly by version, import the types from `@atomicmemory/atomicmemory-sdk/search` and read the shape. The pattern is always: vectorize the query, score it against stored vectors, rank. ## When to graduate to `MemoryClient` @@ -102,5 +102,5 @@ Both modes are legitimate. Pick the one that matches the constraint. ## Next -- [Embeddings](/sdk/concepts/embeddings) — the default model and caching behaviour -- [Storage adapters](/sdk/concepts/storage-adapters) — the adapter interface if `IndexedDBStorageAdapter` doesn't fit +- [Embeddings](/sdk/concepts/embeddings), the default model and caching behaviour +- [Storage adapters](/sdk/concepts/storage-adapters), the adapter interface if `IndexedDBStorageAdapter` doesn't fit diff --git a/docs/sdk/guides/custom-provider.md b/docs/sdk/guides/custom-provider.md index ac2a87a..5f9a8e0 100644 --- a/docs/sdk/guides/custom-provider.md +++ b/docs/sdk/guides/custom-provider.md @@ -5,11 +5,11 @@ sidebar_position: 3 # Writing a custom provider -The provider interface is deliberately small. If you want to back `MemoryClient` with something other than `atomicmemory-core` or Mem0 — an internal memory service, a Pinecone-fronted store, a SQLite-over-disk prototype — you write a `MemoryProvider` and register it. Application code keeps calling the same client methods. +The provider interface is deliberately small. If you want to back `MemoryClient` with something other than `atomicmemory-core` or Mem0, an internal memory service, a Pinecone-fronted store, a SQLite-over-disk prototype, you write a `MemoryProvider` and register it. Application code keeps calling the same client methods. ## Minimum implementation -Extend `BaseMemoryProvider` — it provides the scope-validation scaffolding and a default `getExtension` that checks your declared capabilities. +Extend `BaseMemoryProvider`, it provides the scope-validation scaffolding and a default `getExtension` that checks your declared capabilities. ```typescript import { @@ -71,7 +71,7 @@ export class MyProvider extends BaseMemoryProvider { } ``` -`BaseMemoryProvider` implements the public `ingest` / `search` / `get` / `delete` / `list` methods for you — they run scope validation and error wrapping, then call the protected `do*` hooks above. You only implement the hooks plus `capabilities()` and `name`. Everything else is opt-in. +`BaseMemoryProvider` implements the public `ingest` / `search` / `get` / `delete` / `list` methods for you, they run scope validation and error wrapping, then call the protected `do*` hooks above. You only implement the hooks plus `capabilities()` and `name`. Everything else is opt-in. ## Register the provider @@ -93,7 +93,7 @@ const memory = new MemoryClient({ Two rules: 1. **Don't lie.** If `capabilities().extensions.package` is `true`, your `getExtension('package')` must return a real `Packager`. The default `BaseMemoryProvider.getExtension` returns `this` when the capability is true and relies on the subclass implementing the extension methods on itself. -2. **Declare `requiredScope` honestly.** The shape is `{ default: Array, ingest?: ..., search?: ..., ... }` — a list of required fields for each operation, falling back to `default`. If your backend needs `namespace` to partition correctly, include it in `default` (or the specific operation key). `BaseMemoryProvider.validateScope` will reject requests missing required fields before they reach your network code. +2. **Declare `requiredScope` honestly.** The shape is `{ default: Array, ingest?: ..., search?: ..., ... }`, a list of required fields for each operation, falling back to `default`. If your backend needs `namespace` to partition correctly, include it in `default` (or the specific operation key). `BaseMemoryProvider.validateScope` will reject requests missing required fields before they reach your network code. ## Implementing an extension @@ -113,7 +113,7 @@ export class MyProvider extends BaseMemoryProvider implements Packager { } ``` -Set `capabilities().extensions.package = true`. Done — `memory.package()` now works. +Set `capabilities().extensions.package = true`. Done, `memory.package()` now works. ## Testing @@ -121,5 +121,5 @@ The SDK's own test suite uses fixture providers under `src/memory/__tests__/`. T ## Next -- [Provider model](/sdk/concepts/provider-model) — the full extension menu you can opt into -- [Capabilities](/sdk/concepts/capabilities) — the contract apps rely on when they call your provider +- [Provider model](/sdk/concepts/provider-model), the full extension menu you can opt into +- [Capabilities](/sdk/concepts/capabilities), the contract apps rely on when they call your provider diff --git a/docs/sdk/guides/mem0-backend.md b/docs/sdk/guides/mem0-backend.md index bd80645..b49c0cc 100644 --- a/docs/sdk/guides/mem0-backend.md +++ b/docs/sdk/guides/mem0-backend.md @@ -31,10 +31,10 @@ await memory.initialize(); `Mem0Provider` implements the six core operations (`ingest`, `search`, `get`, `delete`, `list`, `capabilities`) plus the `health` extension. It does **not** implement: -- `package` — context packaging with token budgets -- `temporal` — point-in-time search -- `versioning` — per-memory history -- `update` — in-place updates +- `package`, context packaging with token budgets +- `temporal`, point-in-time search +- `versioning`, per-memory history +- `update`, in-place updates - `graph`, `forget`, `profile`, `reflect`, `batch` Code that calls `memory.package()` on a Mem0-backed client will raise `UnsupportedOperationError`. Use the capability-probing pattern from [Capabilities](/sdk/concepts/capabilities) to handle this gracefully: @@ -61,5 +61,5 @@ For greenfield deployments where packaging, temporal queries, and versioning mat ## Next -- [Swapping backends](/sdk/guides/swapping-backends) — migrating memories between providers -- [Capabilities](/sdk/concepts/capabilities) — the runtime contract that makes provider differences safe +- [Swapping backends](/sdk/guides/swapping-backends), migrating memories between providers +- [Capabilities](/sdk/concepts/capabilities), the runtime contract that makes provider differences safe diff --git a/docs/sdk/guides/nodejs-server.md b/docs/sdk/guides/nodejs-server.md index 590dfac..b876e0b 100644 --- a/docs/sdk/guides/nodejs-server.md +++ b/docs/sdk/guides/nodejs-server.md @@ -5,7 +5,7 @@ sidebar_position: 5 # Node.js / server-side -Using the SDK server-side is supported — the package is ESM + CJS, has no browser-only code on its critical path, and the subpath storage primitives run anywhere Node does. +Using the SDK server-side is supported, the package is ESM + CJS, has no browser-only code on its critical path, and the subpath storage primitives run anywhere Node does. This guide covers the two things that differ from the browser path: storage selection and authorization. @@ -26,11 +26,11 @@ const storage = new StorageManager([adapter]); await storage.initialize(); ``` -Note: `MemoryClient` does not use `StorageManager` internally — storage lives server-side in the active memory backend. Instantiate `StorageManager` only if your app needs its own local key-value store. +Note: `MemoryClient` does not use `StorageManager` internally, storage lives server-side in the active memory backend. Instantiate `StorageManager` only if your app needs its own local key-value store. ## Authorization -`MemoryClient` is a thin HTTP client over the active `MemoryProvider`. It does not enforce any gate before calling the backend. Authorization — who can call which endpoint, for which `scope.user` — is your app's responsibility. +`MemoryClient` is a thin HTTP client over the active `MemoryProvider`. It does not enforce any gate before calling the backend. Authorization, who can call which endpoint, for which `scope.user`, is your app's responsibility. The recommended shape on a server is: @@ -75,9 +75,9 @@ app.get('/memory/:userId/search', async (req, res) => { app.listen(3000); ``` -Validating `req.params.userId` against the authenticated session is a load-bearing step — the SDK trusts whatever scope you hand it. +Validating `req.params.userId` against the authenticated session is a load-bearing step, the SDK trusts whatever scope you hand it. ## Next -- [Scopes and identity](/sdk/concepts/scopes-and-identity) — the `Scope` shape every operation carries -- [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) — production checklist that applies here too +- [Scopes and identity](/sdk/concepts/scopes-and-identity), the `Scope` shape every operation carries +- [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend), production checklist that applies here too diff --git a/docs/sdk/guides/swapping-backends.md b/docs/sdk/guides/swapping-backends.md index 56cbe64..e5b8d85 100644 --- a/docs/sdk/guides/swapping-backends.md +++ b/docs/sdk/guides/swapping-backends.md @@ -11,7 +11,7 @@ Two scenarios come up in practice: runtime swap (flip providers without restarti ## Runtime swap -`MemoryClient` is initialized with a `providers` map and an optional `defaultProvider`; swapping means destroying the current client and constructing a new one. There is no live reconfigure path, by design — providers hold HTTP clients, caches, and init state, and a clean reconstruction is easier to reason about than partial rewiring. +`MemoryClient` is initialized with a `providers` map and an optional `defaultProvider`; swapping means destroying the current client and constructing a new one. There is no live reconfigure path, by design, providers hold HTTP clients, caches, and init state, and a clean reconstruction is easier to reason about than partial rewiring. ```typescript import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; @@ -35,7 +35,7 @@ const memory = await withProvider('atomicmemory'); const memory2 = await withProvider('mem0'); ``` -If you have an in-flight operation on the old client, let it complete before the swap — destroying the client mid-request is not a supported path. +If you have an in-flight operation on the old client, let it complete before the swap, destroying the client mid-request is not a supported path. ## Migration: moving memories between providers @@ -76,18 +76,18 @@ When `source.capabilities().extensions` declares features that `target.capabilit | Source has | Target does not | Consequence | |---|---|---| -| Versioning (version history) | — | Target holds only the latest version; history is dropped | -| Temporal (as-of timestamps) | — | Target loses the temporal metadata; queries like `searchAsOf` will not work | -| Packaging (structured context) | — | No immediate loss on migration; affects read-time behavior only | -| Custom extensions | — | Any data stored via `customExtensions` does not survive | +| Versioning (version history) | Unsupported | Target holds only the latest version; history is dropped | +| Temporal (as-of timestamps) | Unsupported | Target loses the temporal metadata; queries like `searchAsOf` will not work | +| Packaging (structured context) | Unsupported | No immediate loss on migration; affects read-time behavior only | +| Custom extensions | Unsupported | Any data stored via `customExtensions` does not survive | Always run `source.capabilities()` and `target.capabilities()` before migrating and document which dimensions will not survive. A dry-run against a sample is cheap and informative. ## Stateful caches and retries -If your migration job crashes, resume by finding the last successfully-migrated `memory.id` and starting `source.list` from there. The `cursor` returned by `list` is the natural checkpoint — persist it. +If your migration job crashes, resume by finding the last successfully-migrated `memory.id` and starting `source.list` from there. The `cursor` returned by `list` is the natural checkpoint, persist it. ## Next -- [Capabilities](/sdk/concepts/capabilities) — the runtime contract that makes gaps visible -- [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend) — the production checklist that applies to the target side of migrations +- [Capabilities](/sdk/concepts/capabilities), the runtime contract that makes gaps visible +- [Using the atomicmemory backend](/sdk/guides/atomicmemory-backend), the production checklist that applies to the target side of migrations diff --git a/docs/sdk/overview.md b/docs/sdk/overview.md index e3cf585..f9e41b5 100644 --- a/docs/sdk/overview.md +++ b/docs/sdk/overview.md @@ -11,11 +11,11 @@ A TypeScript client for memory, pluggable across backends. ## Three-point value prop -- **Backend-agnostic via `MemoryProvider`.** Every operation routes through the `MemoryProvider` interface. Swap `atomicmemory-core` for Mem0 — or a provider you write — with a config change, not a rewrite. Apps inspect provider capabilities at runtime and gracefully degrade when a backend does not support an extension (packaging, temporal search, versioning, etc.). +- **Backend-agnostic via `MemoryProvider`.** Every operation routes through the `MemoryProvider` interface. Swap `atomicmemory-core` for Mem0, or a provider you write, with a config change, not a rewrite. Apps inspect provider capabilities at runtime and gracefully degrade when a backend does not support an extension (packaging, temporal search, versioning, etc.). - **Browser / extension / Node / worker-ready.** ESM + CJS dual build, subpath exports, and WASM-based local embeddings via `@huggingface/transformers`. The same package runs in a Chrome extension, a Next.js app, a serverless handler, or a web worker. -- **Client-side primitives for composition.** The `/storage`, `/embedding`, and `/search` subpath exports ship standalone — `StorageManager`, `EmbeddingGenerator`, `SemanticSearch`. You can compose memory features directly from these without ever constructing `MemoryClient`. The SDK is a bundle of reusable parts, not a single mandatory object. +- **Client-side primitives for composition.** The `/storage`, `/embedding`, and `/search` subpath exports ship standalone, `StorageManager`, `EmbeddingGenerator`, `SemanticSearch`. You can compose memory features directly from these without ever constructing `MemoryClient`. The SDK is a bundle of reusable parts, not a single mandatory object. -## `MemoryClient` — the canonical public API +## `MemoryClient`, the canonical public API ```ts import { MemoryClient } from '@atomicmemory/atomicmemory-sdk'; @@ -37,7 +37,7 @@ const results = await memory.search({ }); ``` -`MemoryClient` composes a `MemoryService` under the hood and exposes the pure memory API: `ingest`, `search`, `package`, `get`, `list`, `delete`, plus `capabilities` / `getExtension` / `getProviderStatus` for capability inspection, and an `atomicmemory` getter that returns the full [AtomicMemory namespace handle](/sdk/api/memory-provider) (lifecycle, audit, lessons, config, agents). The surface is intentionally small — application-layer concerns such as identity resolution, capture policy, or injection gating belong in consumer code, not in the SDK. +`MemoryClient` composes a `MemoryService` under the hood and exposes the pure memory API: `ingest`, `search`, `package`, `get`, `list`, `delete`, plus `capabilities` / `getExtension` / `getProviderStatus` for capability inspection, and an `atomicmemory` getter that returns the full [AtomicMemory namespace handle](/sdk/api/memory-provider) (lifecycle, audit, lessons, config, agents). The surface is intentionally small, application-layer concerns such as identity resolution, capture policy, or injection gating belong in consumer code, not in the SDK. ## Deployment topologies @@ -59,11 +59,11 @@ flowchart LR end ``` -The first three topologies route through `MemoryClient`. The fourth composes the subpath exports directly — useful when you want client-side semantic search over your app's data without any backend at all. See [Browser primitives](/sdk/guides/browser-primitives). +The first three topologies route through `MemoryClient`. The fourth composes the subpath exports directly, useful when you want client-side semantic search over your app's data without any backend at all. See [Browser primitives](/sdk/guides/browser-primitives). ## Browser-safe entry -For browser applications that talk to a backend-hosted core through `MemoryClient` and don't need on-device storage or embeddings, import from the `./browser` subpath — a slim bundle that omits `storage`, `embedding`, `search`, and `utils`: +For browser applications that talk to a backend-hosted core through `MemoryClient` and don't need on-device storage or embeddings, import from the `./browser` subpath, a slim bundle that omits `storage`, `embedding`, `search`, and `utils`: ```ts import { MemoryClient } from '@atomicmemory/atomicmemory-sdk/browser'; @@ -73,4 +73,4 @@ import { MemoryClient } from '@atomicmemory/atomicmemory-sdk/browser'; - Run through the [Quickstart](/sdk/quickstart) for a 5-minute install and first call. - Read the [Provider model](/sdk/concepts/provider-model) to understand how the backend-agnostic story works in detail. -- If you are looking for the memory engine itself — the server, the HTTP API, the claim schema — start at the [Core introduction](/). +- If you are looking for the memory engine itself, the server, the HTTP API, the claim schema, start at the [Core introduction](/). diff --git a/docs/sdk/quickstart.md b/docs/sdk/quickstart.md index 826a5d7..33ad681 100644 --- a/docs/sdk/quickstart.md +++ b/docs/sdk/quickstart.md @@ -5,14 +5,14 @@ sidebar_position: 2 # SDK Quickstart -Install, initialize, and make your first ingest and search — with `MemoryClient` wired to a running `atomicmemory-core`. +Install, initialize, and make your first ingest and search, with `MemoryClient` wired to a running `atomicmemory-core`. ## Prerequisites - **Node.js ≥ 20** (the SDK is ESM + CJS; the core container runs on `node:22-slim`) -- **A running `atomicmemory-core`** — follow the [Core Quickstart](/quickstart) if you have not yet brought one up; we assume `http://localhost:3050` below +- **A running `atomicmemory-core`**, follow the [Core Quickstart](/quickstart) if you have not yet brought one up; we assume `http://localhost:3050` below -## Step 1 — Install +## Step 1, Install ```bash npm install @atomicmemory/atomicmemory-sdk @@ -20,7 +20,7 @@ npm install @atomicmemory/atomicmemory-sdk Also works with `pnpm add` / `yarn add`. -## Step 2 — Initialize +## Step 2, Initialize `MemoryClient` needs one thing: a `providers` map telling it which memory backend(s) to use and how to reach them. @@ -50,7 +50,7 @@ new MemoryClient({ }); ``` -## Step 3 — Ingest a memory +## Step 3, Ingest a memory ```typescript await memory.ingest({ @@ -76,7 +76,7 @@ await memory.ingest({ `ingest` takes a single `IngestInput`. Any capture-gating policy ("should this conversation be saved at all?") lives in your application, not in the SDK. -## Step 4 — Search +## Step 4, Search ```typescript const page = await memory.search({ @@ -94,7 +94,7 @@ Similarly, `search` takes a single `SearchRequest`. Injection-gating ("should th ## This is the whole round trip -`initialize` → `ingest` → `search`. Every other method (`get`, `delete`, `list`, `package`, `capabilities`, `getExtension`, `getProviderStatus`) is reachable on the same `memory` object. Backend-specific admin — lifecycle reset, audit trails, lesson lists, agent trust — is reachable via the `atomicmemory` namespace handle: +`initialize` → `ingest` → `search`. Every other method (`get`, `delete`, `list`, `package`, `capabilities`, `getExtension`, `getProviderStatus`) is reachable on the same `memory` object. Backend-specific admin, lifecycle reset, audit trails, lesson lists, agent trust, is reachable via the `atomicmemory` namespace handle: ```typescript const handle = memory.atomicmemory; diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 5e14a41..5b49a7e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -3,6 +3,7 @@ import type {Config} from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; +import llmsDirective from './src/remark/llms-directive.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); // OpenAPI spec is vendored into this repo under `vendor/` so the docs @@ -16,9 +17,13 @@ const coreOpenapiYamlPath = resolve( 'vendor/atomicmemory-core-openapi.yaml', ); +// Single source of truth for the canonical site origin. +const SITE_URL = 'https://docs.atomicmemory.ai'; +const BASE_URL = '/'; + const config: Config = { title: 'AtomicMemory', - tagline: 'Standardized platform layer for AI memory — pluggable at every seam.', + tagline: 'Standardized platform layer for AI memory, pluggable at every seam.', favicon: 'img/logo.svg', headTags: [ @@ -44,9 +49,9 @@ const config: Config = { v4: true, }, - // Production URL — pointed at docs.atomicmemory.ai via CNAME - url: 'https://docs.atomicmemory.ai', - baseUrl: '/', + // Production URL, pointed at docs.atomicmemory.ai via CNAME + url: SITE_URL, + baseUrl: BASE_URL, // GitHub Pages deployment config organizationName: 'atomicmemory', @@ -64,6 +69,7 @@ const config: Config = { themes: ['@docusaurus/theme-mermaid', 'docusaurus-theme-openapi-docs'], plugins: [ + './src/plugins/llms-and-mirror-plugin.mjs', [ '@docusaurus/plugin-client-redirects', { @@ -80,7 +86,7 @@ const config: Config = { config: { atomicmemory: { // Source-of-truth is @atomicmemory/atomicmemory-core's - // shipped openapi.yaml — regenerated from Zod schemas on + // shipped openapi.yaml, regenerated from Zod schemas on // every core publish. specPath: coreOpenapiYamlPath, outputDir: 'docs/api-reference/http', @@ -111,6 +117,11 @@ const config: Config = { // .mdx pages can use the `api` doc type for its request / // response renderers. docItemComponent: '@theme/ApiItem', + // Inject the AFDocs llms.txt directive into every doc page. + // Runs at MDX parse time so it covers both hand-authored + // pages and the OpenAPI plugin's generated `.api.mdx` files + // (which use `` JSX, not markdown `#`). + beforeDefaultRemarkPlugins: [llmsDirective], }, blog: false, theme: { diff --git a/package-lock.json b/package-lock.json index 7beadea..3ae3cae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,13 @@ { "name": "atomicmemory-docs", - "version": "0.0.0", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "atomicmemory-docs", - "version": "0.0.0", + "version": "1.0.0", + "license": "Apache-2.0", "dependencies": { "@docusaurus/core": "3.10.0", "@docusaurus/faster": "3.10.0", @@ -25,7 +26,9 @@ "@docusaurus/module-type-aliases": "3.10.0", "@docusaurus/tsconfig": "3.10.0", "@docusaurus/types": "3.10.0", + "@types/js-yaml": "^4.0.9", "@types/react": "^19.0.0", + "js-yaml": "^4.1.1", "typescript": "~6.0.2" }, "engines": { @@ -168,7 +171,6 @@ "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.50.2.tgz", "integrity": "sha512-ypSboUJ3XJoQz5DeDo82hCnrRuwq3q9ZdFhVKAik9TnZh1DvLqoQsrbBjXg7C7zQOtV/Qbge/HmyoV6V5L7MhQ==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/client-common": "5.50.2", "@algolia/requester-browser-xhr": "5.50.2", @@ -322,7 +324,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -2156,7 +2157,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -2179,7 +2179,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -2289,7 +2288,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -2711,7 +2709,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -3542,7 +3539,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.10.0.tgz", "integrity": "sha512-mgLdQsO8xppnQZc3LPi+Mf+PkPeyxJeIx11AXAq/14fsaMefInQiMEZUUmrc7J+956G/f7MwE7tn8KZgi3iRcA==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/babel": "3.10.0", "@docusaurus/bundler": "3.10.0", @@ -3625,7 +3621,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/faster/-/faster-3.10.0.tgz", "integrity": "sha512-GNPtVH14ISjHfSwnHu3KiFGf86ICmJSQDeSv/QaanpBgiZGOtgZaslnC5q8WiguxM1EVkwcGxPuD8BXF4eggKw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/types": "3.10.0", "@rspack/core": "^1.7.10", @@ -3780,7 +3775,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.10.0.tgz", "integrity": "sha512-9BjHhf15ct8Z7TThTC0xRndKDVvMKmVsAGAN7W9FpNRzfMdScOGcXtLmcCWtJGvAezjOJIm6CxOYCy3Io5+RnQ==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/core": "3.10.0", "@docusaurus/logger": "3.10.0", @@ -4050,7 +4044,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.10.0.tgz", "integrity": "sha512-Dkp1YXKn16ByCJAdIjbDIOpVb4Z66MsVD694/ilX1vAAHaVEMrVsf/NPd9VgreyFx08rJ9GqV1MtzsbTcU73Kg==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/mdx-loader": "3.10.0", "@docusaurus/module-type-aliases": "3.10.0", @@ -4195,7 +4188,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.10.0.tgz", "integrity": "sha512-T3B0WTigsIthe0D4LQa2k+7bJY+c3WS+Wq2JhcznOSpn1lSN64yNtHQXboCj3QnUs1EuAZszQG1SHKu5w5ZrlA==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/logger": "3.10.0", "@docusaurus/types": "3.10.0", @@ -4241,7 +4233,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.10.0.tgz", "integrity": "sha512-c+6n2+ZPOJtWWc8Bb/EYdpSDfjYEScdCu9fB/SNjOmSCf1IdVnGf2T53o0tsz0gDRtCL90tifTL0JE/oMuP1Mw==", "license": "MIT", - "peer": true, "dependencies": { "@docusaurus/logger": "3.10.0", "@docusaurus/utils": "3.10.0", @@ -4884,7 +4875,6 @@ "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", "license": "MIT", - "peer": true, "dependencies": { "@types/mdx": "^2.0.0" }, @@ -6004,7 +5994,6 @@ "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/core": "^7.21.3", "@svgr/babel-preset": "8.1.0", @@ -6109,7 +6098,6 @@ "integrity": "sha512-R8VQbQY1BZcbIF2p3gjlTCwAQzx1A194ugWfwld5y+WgVVWqVKm7eURGGOVbQVubgKWzidP2agomBbg96rZilQ==", "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.26" @@ -7028,12 +7016,18 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@types/mdast": { "version": "4.0.4", @@ -7094,7 +7088,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -7459,7 +7452,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -7528,7 +7520,6 @@ "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.18.0.tgz", "integrity": "sha512-F+LMD2IDIXuHxgpLJh3nkLj9+tSaEzoUWd+7fONGq5pe2169FUDjpEkOfEpoGLz1sbZni/69p07OsecNfAOpqA==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -7588,7 +7579,6 @@ "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.50.2.tgz", "integrity": "sha512-Tfp26yoNWurUjfgK4GOrVJQhSNXu9tJtHfFFNosgT2YClG+vPyUjX/gbC8rG39qLncnZg8Fj34iarQWpMkqefw==", "license": "MIT", - "peer": true, "dependencies": { "@algolia/abtesting": "1.16.2", "@algolia/client-abtesting": "5.50.2", @@ -8136,7 +8126,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -8484,7 +8473,6 @@ "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@chevrotain/cst-dts-gen": "12.0.0", "@chevrotain/gast": "12.0.0", @@ -9264,7 +9252,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -9584,7 +9571,6 @@ "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10" } @@ -9994,7 +9980,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -11321,7 +11306,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -16432,7 +16416,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17236,7 +17219,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -18140,7 +18122,6 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "license": "MIT", - "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -19056,7 +19037,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -19066,7 +19046,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -19103,7 +19082,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.72.1.tgz", "integrity": "sha512-RhwBoy2ygeVZje+C+bwJ8g0NjTdBmDlJvAUHTxRjTmSUKPYsKfMphkS2sgEMotsY03bP358yEYlnUeZy//D9Ig==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -19164,7 +19142,6 @@ "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", "license": "MIT", - "peer": true, "dependencies": { "@types/react": "*" }, @@ -19242,7 +19219,6 @@ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", "license": "MIT", - "peer": true, "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" @@ -19266,7 +19242,6 @@ "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.12.13", "history": "^4.9.0", @@ -19421,8 +19396,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-thunk": { "version": "3.1.0", @@ -20026,7 +20000,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", "license": "MIT", - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", @@ -21376,7 +21349,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -21490,8 +21462,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD", - "peer": true + "license": "0BSD" }, "node_modules/tsyringe": { "version": "4.10.0", @@ -21572,7 +21543,6 @@ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -21933,7 +21903,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -22257,7 +22226,6 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.106.2.tgz", "integrity": "sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==", "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", diff --git a/package.json b/package.json index cfa32d9..41aa9b3 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,7 @@ "license": "Apache-2.0", "scripts": { "docusaurus": "docusaurus", - "prestart": "npm run regen:api", "start": "docusaurus start --port 3013", - "prebuild": "npm run regen:api", "build": "docusaurus build", "swizzle": "docusaurus swizzle", "deploy": "docusaurus deploy", @@ -39,7 +37,9 @@ "@docusaurus/module-type-aliases": "3.10.0", "@docusaurus/tsconfig": "3.10.0", "@docusaurus/types": "3.10.0", + "@types/js-yaml": "^4.0.9", "@types/react": "^19.0.0", + "js-yaml": "^4.1.1", "typescript": "~6.0.2" }, "browserslist": { diff --git a/scripts/build-llms-txt.mjs b/scripts/build-llms-txt.mjs new file mode 100644 index 0000000..d6a7f0b --- /dev/null +++ b/scripts/build-llms-txt.mjs @@ -0,0 +1,198 @@ +/** + * @file llms.txt + llms-full.txt generators (AFDocs compliance). + * + * `llms.txt`, concise spec-compliant index per https://llmstxt.org: + * H1, blockquote summary, H2 sections, bulleted markdown links to + * each page with a one-sentence description. Sections are derived + * from a hand-maintained URL-prefix → label map (small, stable; new + * top-level sections are a one-line update). Pages with permalinks + * that don't match any prefix land in `## Other`. + * + * `llms-full.txt`, full-corpus snapshot, driven by the + * `permalink → canonical-mirror-path` map returned by + * `mirror-markdown.mjs`. One entry per route, separated by a + * standard `---` rule. We never walk the filesystem here, so the + * `.md` / `/index.md` mirror pair never produces + * duplicate corpus entries. + */ + +import fs from "node:fs/promises"; +import path from "node:path"; + +/** + * Hand-maintained section map. Order determines section order in the + * emitted llms.txt. Each entry's URL prefixes are matched in order; + * the first match wins. + */ +const SECTIONS = [ + { label: "Get started", prefixes: ["/quickstart", "/"] }, + { label: "Platform", prefixes: ["/platform/"] }, + { label: "SDK", prefixes: ["/sdk/"] }, + { label: "Integrations", prefixes: ["/integrations/"] }, + { label: "API Reference", prefixes: ["/api-reference/"] }, +]; + +const FALLBACK_LABEL = "Other"; + +/** + * @param {object} args + * @param {string} args.outDir + * @param {string} args.siteUrl trailing-slash form, e.g. https://docs.atomicmemory.ai/ + * @param {string} args.siteTitle + * @param {string} [args.siteTagline] + * @param {Array} args.docs + */ +export async function buildLlmsTxt({ outDir, siteUrl, siteTitle, siteTagline, docs }) { + const sectioned = groupBySection(docs); + const lines = []; + lines.push(`# ${siteTitle}`); + lines.push(""); + if (siteTagline) { + lines.push(`> ${siteTagline}`); + lines.push(""); + } + + for (const section of SECTIONS) { + const entries = sectioned.get(section.label); + if (!entries || entries.length === 0) continue; + lines.push(`## ${section.label}`); + lines.push(""); + for (const entry of entries) { + lines.push(formatBulletEntry(entry, siteUrl)); + } + lines.push(""); + } + + const fallback = sectioned.get(FALLBACK_LABEL); + if (fallback && fallback.length > 0) { + lines.push(`## ${FALLBACK_LABEL}`); + lines.push(""); + for (const entry of fallback) { + lines.push(formatBulletEntry(entry, siteUrl)); + } + lines.push(""); + } + + lines.push("## Optional"); + lines.push(""); + lines.push(`- [skill.md](${new URL("skill.md", siteUrl).toString()}): Operating guide for agents reading these docs, when to read, how to navigate, citation conventions.`); + lines.push(`- [.well-known/mcp.json](${new URL(".well-known/mcp.json", siteUrl).toString()}): MCP server descriptor (currently local-install / stdio).`); + lines.push(""); + + await fs.writeFile(path.join(outDir, "llms.txt"), lines.join("\n"), "utf8"); +} + +function formatBulletEntry(doc, siteUrl) { + const permalinkRel = doc.permalink.replace(/^\//, ""); + // Root permalink ("/") still has a markdown mirror at /index.md; + // point at it explicitly so every entry is consistently a `.md` + // link (avoids requiring HTML scraping for the home page, + // especially while content negotiation is deferred). + const mdRelative = permalinkRel === "" ? "index.md" : `${permalinkRel}.md`; + const url = new URL(mdRelative, siteUrl).toString(); + const title = doc.title || doc.id; + const desc = oneSentence(doc.description); + return desc + ? `- [${title}](${url}): ${desc}` + : `- [${title}](${url})`; +} + +function oneSentence(description) { + if (!description) return ""; + const trimmed = description.trim(); + if (trimmed === "") return ""; + const idx = trimmed.search(/[.!?](\s|$)/); + if (idx === -1) return trimmed; + return trimmed.slice(0, idx + 1); +} + +/** + * @param {Array} docs + * @returns {Map>} + */ +function groupBySection(docs) { + const out = new Map(); + for (const section of SECTIONS) { + out.set(section.label, []); + } + out.set(FALLBACK_LABEL, []); + + for (const doc of docs) { + const label = sectionLabelFor(doc.permalink); + out.get(label).push(doc); + } + + for (const list of out.values()) { + list.sort(byPermalink); + } + return out; +} + +function sectionLabelFor(permalink) { + for (const section of SECTIONS) { + for (const prefix of section.prefixes) { + if (prefix === "/") { + if (permalink === "/") return section.label; + continue; + } + if (permalink === prefix || permalink.startsWith(prefix)) { + return section.label; + } + } + } + return FALLBACK_LABEL; +} + +function byPermalink(a, b) { + return a.permalink.localeCompare(b.permalink); +} + +/** + * Build llms-full.txt from the canonical mirror map. Emits a heading + * with the page URL + permalink for each route, followed by the + * mirrored markdown body, separated by `---`. + * + * @param {object} args + * @param {string} args.outDir + * @param {string} args.siteUrl + * @param {string} args.siteTitle + * @param {Array} args.docs + * @param {Map} args.canonicalMirrorByPermalink + */ +export async function buildLlmsFullTxt({ + outDir, + siteUrl, + siteTitle, + docs, + canonicalMirrorByPermalink, +}) { + const sortedDocs = [...docs].sort(byPermalink); + const segments = []; + segments.push(`# ${siteTitle}, full corpus`); + segments.push(""); + segments.push(`> Generated snapshot of every page on ${siteUrl}, in stable URL order. Use llms.txt for the index.`); + segments.push(""); + + for (const doc of sortedDocs) { + const mirror = canonicalMirrorByPermalink.get(doc.permalink); + if (!mirror) continue; + const body = await fs.readFile(path.join(outDir, mirror), "utf8"); + const url = doc.permalink === "/" + ? siteUrl + : new URL(doc.permalink.replace(/^\//, ""), siteUrl).toString(); + segments.push(`## ${doc.title || doc.id}`); + segments.push(""); + segments.push(`Source: ${url}`); + segments.push(""); + segments.push(body.trim()); + segments.push(""); + segments.push("---"); + segments.push(""); + } + + await fs.writeFile( + path.join(outDir, "llms-full.txt"), + segments.join("\n"), + "utf8", + ); +} diff --git a/scripts/build-skill-md.mjs b/scripts/build-skill-md.mjs new file mode 100644 index 0000000..c17e049 --- /dev/null +++ b/scripts/build-skill-md.mjs @@ -0,0 +1,30 @@ +/** + * @file `build/skill.md` guard. + * + * Docusaurus copies `static/` to the build output root automatically, + * so `static/skill.md` lands at `build/skill.md` without any work. + * This guard explicitly verifies the file exists and re-copies it + * from `static/skill.md` if a future Docusaurus change breaks the + * default mirror, failing loud rather than shipping a docs build + * with a missing AFDocs artifact. + */ + +import fs from "node:fs/promises"; +import path from "node:path"; + +/** + * @param {object} args + * @param {string} args.outDir + * @param {string} args.siteDir + */ +export async function ensureSkillMd({ outDir, siteDir }) { + const target = path.join(outDir, "skill.md"); + try { + const stat = await fs.stat(target); + if (stat.isFile() && stat.size > 0) return; + } catch { + // fall through to recopy + } + const source = path.join(siteDir, "static", "skill.md"); + await fs.copyFile(source, target); +} diff --git a/scripts/mirror-markdown.mjs b/scripts/mirror-markdown.mjs new file mode 100644 index 0000000..e1b77b3 --- /dev/null +++ b/scripts/mirror-markdown.mjs @@ -0,0 +1,286 @@ +/** + * @file Markdown URL mirror generator for AFDocs compliance. + * + * Writes a `.md` sibling for every Docusaurus route at both URL shapes + * (`build/.md` and `build//index.md`) so an agent can + * append `.md` to any documentation URL and fetch the markdown form + * directly. Hand-authored pages copy from the MDX source (front-matter + * and import / JSX expressions stripped); OpenAPI pages are rendered + * from the vendored OpenAPI YAML, joined to Docusaurus slugs via + * `kebabCase(operationId)`. + * + * Returns a `permalink → canonical-mirror-path` map so the llms-full + * generator can iterate routes once and avoid double-counting the + * `.md` / `/index.md` pair. + */ + +import fs from "node:fs/promises"; +import path from "node:path"; +import yaml from "js-yaml"; + +const SITE_PREFIX = "@site/"; +const OPENAPI_VENDOR_PATH = "vendor/atomicmemory-core-openapi.yaml"; +const API_REFERENCE_PREFIX = "/api-reference/http/"; + +/** + * @param {object} args + * @param {string} args.outDir + * @param {string} args.siteDir + * @param {Array} args.docs + */ +export async function mirrorMarkdown({ outDir, siteDir, docs }) { + const { operationBySlug, info: openapiInfo } = await loadOpenapiSpec({ siteDir }); + const canonicalMirrorByPermalink = new Map(); + + for (const doc of docs) { + const body = await renderDocBody({ siteDir, doc, operationBySlug, openapiInfo }); + if (body === null) continue; + const canonicalMirror = await writeMirrorPair({ + outDir, + permalink: doc.permalink, + body, + }); + canonicalMirrorByPermalink.set(doc.permalink, canonicalMirror); + } + + return { canonicalMirrorByPermalink }; +} + +/** + * @param {object} args + * @param {string} args.outDir + * @param {string} args.permalink + * @param {string} args.body + * @returns {Promise} canonical mirror path (relative to outDir) + */ +async function writeMirrorPair({ outDir, permalink, body }) { + const trimmed = permalink.replace(/^\/+|\/+$/g, ""); + const isRoot = trimmed === ""; + + const flatRel = isRoot ? "index.md" : `${trimmed}.md`; + const indexRel = isRoot ? "index.md" : `${trimmed}/index.md`; + + await writeFile(outDir, flatRel, body); + if (flatRel !== indexRel) { + await writeFile(outDir, indexRel, body); + } + return flatRel; +} + +async function writeFile(outDir, relPath, body) { + const target = path.join(outDir, relPath); + await fs.mkdir(path.dirname(target), { recursive: true }); + await fs.writeFile(target, body, "utf8"); +} + +/** + * Render the markdown body for a single doc. Returns `null` if the + * doc is an OpenAPI page that has no matching operation in the + * vendored YAML (skip rather than emit a misleading mirror). + * + * @param {object} args + * @param {string} args.siteDir + * @param {import('./types.mjs').LoadedDoc} args.doc + * @param {Map} args.operationBySlug + * @param {import('./types.mjs').OpenApiInfo} args.openapiInfo + * @returns {Promise} + */ +async function renderDocBody({ siteDir, doc, operationBySlug, openapiInfo }) { + if (doc.source && doc.source.endsWith(".info.mdx")) { + return renderOpenapiInfo({ doc, info: openapiInfo }); + } + if (doc.permalink.startsWith(API_REFERENCE_PREFIX)) { + const slug = doc.permalink.slice(API_REFERENCE_PREFIX.length).replace(/\/$/, ""); + const operation = operationBySlug.get(slug); + if (operation) { + return renderOpenapiOperation({ doc, operation }); + } + // Not a per-operation page (e.g. /api-reference/http/conventions). + // Fall through to source-MDX path. + } + return readSourceMdx({ siteDir, doc }); +} + +async function readSourceMdx({ siteDir, doc }) { + const rel = doc.source.startsWith(SITE_PREFIX) + ? doc.source.slice(SITE_PREFIX.length) + : doc.source; + const absolute = path.resolve(siteDir, rel); + const raw = await fs.readFile(absolute, "utf8"); + return stripMdxNoise(raw); +} + +/** + * Remove YAML frontmatter, ESM import statements, and standalone MDX + * comment lines from the file body. Leaves headings, prose, fenced + * code, and inline JSX intact, the resulting markdown is good enough + * for agent consumption even if some JSX components don't render + * standalone. + */ +function stripMdxNoise(raw) { + let body = raw; + + if (body.startsWith("---\n")) { + const end = body.indexOf("\n---", 4); + if (end !== -1) { + body = body.slice(end + 4).replace(/^\s*\n/, ""); + } + } + body = body.replace(/^\s*import\s.+?;\s*$/gm, ""); + body = body.replace(/^\s*\{\/\*[\s\S]*?\*\/\}\s*$/gm, ""); + return body.replace(/\n{3,}/g, "\n\n").trim() + "\n"; +} + +/** + * Load the vendored OpenAPI spec once and return both the per-operation + * lookup (slugs are the kebab-case form of camelCase `operationId`, + * matching the filenames the OpenAPI plugin emits) and the top-level + * `info` block, used to render the rolled-up `*.info.mdx` API page. + * + * @param {object} args + * @param {string} args.siteDir + * @returns {Promise<{operationBySlug: Map, info: import('./types.mjs').OpenApiInfo}>} + */ +async function loadOpenapiSpec({ siteDir }) { + const yamlPath = path.resolve(siteDir, OPENAPI_VENDOR_PATH); + const text = await fs.readFile(yamlPath, "utf8"); + /** @type {any} */ + const spec = yaml.load(text); + const operationBySlug = new Map(); + const info = spec && typeof spec.info === "object" ? spec.info : {}; + if (!spec || typeof spec.paths !== "object") return { operationBySlug, info }; + + for (const [routePath, methods] of Object.entries(spec.paths)) { + if (!methods || typeof methods !== "object") continue; + for (const [method, op] of Object.entries(methods)) { + if (!op || typeof op !== "object") continue; + const opId = /** @type {any} */ (op).operationId; + if (typeof opId !== "string") continue; + const slug = camelToKebab(opId); + operationBySlug.set(slug, { + slug, + operationId: opId, + method: method.toUpperCase(), + path: routePath, + operation: /** @type {any} */ (op), + }); + } + } + return { operationBySlug, info }; +} + +function camelToKebab(camel) { + return camel.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase(); +} + +/** + * Render a single OpenAPI operation as agent-friendly markdown: + * title (H1), method + path, description, parameters table, request + * body shape (when JSON), and status-code summary. Reuses the doc's + * frontmatter title where present so the H1 matches the site. + * + * @param {object} args + * @param {import('./types.mjs').LoadedDoc} args.doc + * @param {import('./types.mjs').OpenApiOperation} args.operation + */ +function renderOpenapiOperation({ doc, operation }) { + const op = operation.operation; + const lines = []; + lines.push(`# ${doc.title || op.summary || operation.operationId}`); + lines.push(""); + lines.push(`> Machine-readable: this page mirrors OpenAPI operation \`${operation.operationId}\`.`); + lines.push(""); + lines.push(`**${operation.method}** \`${operation.path}\``); + lines.push(""); + if (op.description) { + lines.push(op.description.trim()); + lines.push(""); + } else if (op.summary) { + lines.push(op.summary.trim()); + lines.push(""); + } + + const params = renderParameters(op.parameters); + if (params) lines.push(params, ""); + + const body = renderRequestBody(op.requestBody); + if (body) lines.push(body, ""); + + const responses = renderResponses(op.responses); + if (responses) lines.push(responses, ""); + + return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n"; +} + +function renderParameters(parameters) { + if (!Array.isArray(parameters) || parameters.length === 0) return null; + const rows = ["## Parameters", "", "| In | Name | Required | Type | Description |", "|---|---|---|---|---|"]; + for (const param of parameters) { + const schema = param.schema || {}; + rows.push( + `| ${param.in ?? ""} | \`${param.name ?? ""}\` | ${param.required ? "yes" : "no"} | ${schema.type ?? schema.$ref ?? ""} | ${(param.description ?? "").replace(/\n/g, " ")} |`, + ); + } + return rows.join("\n"); +} + +function renderRequestBody(requestBody) { + if (!requestBody || typeof requestBody !== "object") return null; + const content = requestBody.content; + if (!content || typeof content !== "object") return null; + const json = content["application/json"]; + if (!json || !json.schema) return "## Request Body\n\nSee operation schema in the OpenAPI spec."; + const schemaJson = JSON.stringify(json.schema, null, 2); + return `## Request Body (application/json)\n\n\`\`\`json\n${schemaJson}\n\`\`\``; +} + +function renderResponses(responses) { + if (!responses || typeof responses !== "object") return null; + const rows = ["## Responses", "", "| Status | Description |", "|---|---|"]; + for (const [code, response] of Object.entries(responses)) { + const desc = (response && typeof response === "object" && response.description) || ""; + rows.push(`| ${code} | ${desc.replace(/\n/g, " ")} |`); + } + return rows.join("\n"); +} + +/** + * Render the rolled-up "API info" page (the OpenAPI plugin's + * `*.info.mdx` artifact) directly from `spec.info`. The source `.info.mdx` + * is almost entirely JSX components and would produce a useless mirror + * if we fell through to the generic source-MDX path. + * + * @param {object} args + * @param {import('./types.mjs').LoadedDoc} args.doc + * @param {import('./types.mjs').OpenApiInfo} args.info + */ +function renderOpenapiInfo({ doc, info }) { + const lines = []; + lines.push(`# ${info.title || doc.title || "API"}`); + lines.push(""); + lines.push(`> Machine-readable: this page mirrors the OpenAPI \`info\` block from the vendored spec.`); + lines.push(""); + if (info.version) { + lines.push(`Version: \`${info.version}\``); + lines.push(""); + } + if (info.description) { + lines.push(info.description.trim()); + lines.push(""); + } + if (info.license && (info.license.name || info.license.url)) { + const name = info.license.name || "License"; + const url = info.license.url; + lines.push(`License: ${url ? `[${name}](${url})` : name}`); + lines.push(""); + } + if (info.contact && (info.contact.name || info.contact.email || info.contact.url)) { + const parts = []; + if (info.contact.name) parts.push(info.contact.name); + if (info.contact.email) parts.push(`<${info.contact.email}>`); + if (info.contact.url) parts.push(info.contact.url); + lines.push(`Contact: ${parts.join(", ")}`); + lines.push(""); + } + return lines.join("\n").replace(/\n{3,}/g, "\n\n").trim() + "\n"; +} diff --git a/sidebars.ts b/sidebars.ts index 8738232..49cf726 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -5,7 +5,7 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; import httpApiSidebar from './docs/api-reference/http/sidebar'; const sidebars: SidebarsConfig = { - // Platform concepts — the "why modular" story + // Platform concepts, the "why modular" story platformSidebar: [ 'introduction', 'quickstart', @@ -25,7 +25,7 @@ const sidebars: SidebarsConfig = { }, ], - // SDK — the TypeScript client, pluggable across memory backends + // SDK, the TypeScript client, pluggable across memory backends sdkSidebar: [ 'sdk/overview', 'sdk/quickstart', @@ -68,7 +68,7 @@ const sidebars: SidebarsConfig = { }, ], - // Integrations — coding agents (Claude Code, OpenClaw, Codex, Cursor) + // Integrations, coding agents (Claude Code, OpenClaw, Codex, Cursor) // and AI frameworks (Vercel AI SDK, LangChain JS, Mastra, …) that // consume AtomicMemory through the shared MCP server + per-tool // plugin wrappers shipped from Atomicmemory-integrations. @@ -99,7 +99,7 @@ const sidebars: SidebarsConfig = { }, ], - // HTTP API reference — generated from @atomicmemory/atomicmemory-core's + // HTTP API reference, generated from @atomicmemory/atomicmemory-core's // OpenAPI spec by docusaurus-plugin-openapi-docs. The `conventions` // intro page stays hand-maintained and anchors the section. apiReferenceSidebar: [ diff --git a/src/css/custom.css b/src/css/custom.css index 2810cf5..6cb26bc 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -6,7 +6,7 @@ */ :root { - /* Brand palette — #3A60E4 and shades */ + /* Brand palette, #3A60E4 and shades */ --ifm-color-primary: #3a60e4; --ifm-color-primary-dark: #2a52df; --ifm-color-primary-darker: #244dd4; @@ -15,7 +15,7 @@ --ifm-color-primary-lighter: #6a8aec; --ifm-color-primary-lightest: #9db3f2; - /* Typography — mirror webapp: Work Sans display, Fira Mono for code */ + /* Typography, mirror webapp: Work Sans display, Fira Mono for code */ --ifm-font-family-base: 'Work Sans', ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; @@ -25,7 +25,7 @@ --ifm-heading-font-family: var(--ifm-font-family-base); --ifm-font-color-base: #1e252e; /* webapp primary text */ - /* Corners — webapp uses 0.625rem radius */ + /* Corners, webapp uses 0.625rem radius */ --ifm-global-radius: 0.625rem; --ifm-button-border-radius: 0.625rem; --ifm-card-border-radius: 0.625rem; @@ -50,7 +50,7 @@ --docusaurus-highlighted-code-line-bg: rgba(106, 138, 236, 0.2); } -/* Navbar — keep logo crisp, add subtle bottom border like webapp header */ +/* Navbar, keep logo crisp, add subtle bottom border like webapp header */ .navbar { box-shadow: 0 1px 0 rgba(0, 0, 0, 0.06); } diff --git a/src/plugins/llms-and-mirror-plugin.mjs b/src/plugins/llms-and-mirror-plugin.mjs new file mode 100644 index 0000000..bc53515 --- /dev/null +++ b/src/plugins/llms-and-mirror-plugin.mjs @@ -0,0 +1,86 @@ +/** + * @file Docusaurus plugin: AFDocs-compliant static artifacts. + * + * Generates four artifacts during the docs build: + * - `build/.md` and `build//index.md`, markdown + * mirrors for every doc page so agents can append `.md` to any + * URL and fetch the source content directly. + * - `build/llms.txt`, concise spec-compliant index per + * llmstxt.org, with H2 sections derived from a hand-maintained + * URL-prefix → label map. + * - `build/llms-full.txt`, full corpus, one entry per route, built + * from the canonical-mirror map returned by the markdown + * generator (no filesystem walk → no `.md` / + * `/index.md` double-counting). + * - `build/skill.md`, verified to have landed (Docusaurus copies + * `static/` automatically; this is a guard). + * + * The plugin captures docs-plugin content via `allContentLoaded` + * because `postBuild`'s `props` only carries this plugin's own + * content, not the docs plugin's loaded documents (title / + * description / permalink / source / sidebar live there). + */ + +import { mirrorMarkdown } from "../../scripts/mirror-markdown.mjs"; +import { buildLlmsTxt, buildLlmsFullTxt } from "../../scripts/build-llms-txt.mjs"; +import { ensureSkillMd } from "../../scripts/build-skill-md.mjs"; + +/** + * @param {import('@docusaurus/types').LoadContext} context + * @returns {import('@docusaurus/types').Plugin} + */ +export default function llmsAndMirrorPlugin(context) { + /** @type {Array} */ + let loadedDocs = []; + + return { + name: "atomicmemory-llms-and-mirror", + + async allContentLoaded({ allContent }) { + const docsContent = allContent["docusaurus-plugin-content-docs"]; + if (!docsContent) return; + const defaultPlugin = docsContent.default; + if (!defaultPlugin || !Array.isArray(defaultPlugin.loadedVersions)) { + return; + } + const docs = []; + for (const version of defaultPlugin.loadedVersions) { + if (!Array.isArray(version.docs)) continue; + for (const doc of version.docs) { + docs.push({ + id: doc.id, + title: doc.title, + description: doc.description ?? "", + permalink: doc.permalink, + source: doc.source, + sidebar: doc.sidebar ?? null, + }); + } + } + loadedDocs = docs; + }, + + async postBuild({ outDir, siteConfig }) { + const { canonicalMirrorByPermalink } = await mirrorMarkdown({ + outDir, + siteDir: context.siteDir, + docs: loadedDocs, + }); + await buildLlmsTxt({ + outDir, + siteUrl: siteConfig.url + siteConfig.baseUrl, + siteTitle: siteConfig.title, + siteTagline: siteConfig.tagline, + docs: loadedDocs, + }); + await buildLlmsFullTxt({ + outDir, + siteUrl: siteConfig.url + siteConfig.baseUrl, + siteTitle: siteConfig.title, + docs: loadedDocs, + canonicalMirrorByPermalink, + }); + await ensureSkillMd({ outDir, siteDir: context.siteDir }); + }, + }; +} diff --git a/src/remark/llms-directive.mjs b/src/remark/llms-directive.mjs new file mode 100644 index 0000000..aa9bd12 --- /dev/null +++ b/src/remark/llms-directive.mjs @@ -0,0 +1,109 @@ +/** + * @file Remark plugin: inject the AFDocs `llms.txt` directive on every doc page. + * + * Inserts a blockquote linking to `llms.txt` and `llms-full.txt` + * immediately after the first H1, both ordinary markdown headings + * (`# Title`) and the `` JSX node that + * `docusaurus-plugin-openapi-docs` emits for API reference pages. + * + * The directive uses Docusaurus's `pathname://` escape hatch so links + * render as site-root paths without being resolved as docs-plugin + * markdown files before postBuild artifacts exist. + * + * Idempotency: every injected blockquote carries a + * `data-llms-directive` HAST property; the plugin no-ops if it sees + * that marker on a top-level blockquote. + */ + +const DIRECTIVE_MARKER = "data-llms-directive"; + +/** + * @returns {(tree: import('mdast').Root) => void} + */ +export default function llmsDirective() { + return (tree) => { + if (alreadyInjected(tree)) return; + const insertIndex = findInsertIndex(tree); + tree.children.splice( + insertIndex, + 0, + buildDirective(), + ); + }; +} + +function alreadyInjected(tree) { + for (const node of tree.children) { + if (node.type !== "blockquote") continue; + const props = node.data && node.data.hProperties; + if (props && props[DIRECTIVE_MARKER]) return true; + } + return false; +} + +function findInsertIndex(tree) { + for (let i = 0; i < tree.children.length; i += 1) { + const node = tree.children[i]; + if (isMarkdownH1(node) || isJsxH1(node)) { + return i + 1; + } + } + return 0; +} + +function isMarkdownH1(node) { + return node.type === "heading" && node.depth === 1; +} + +function isJsxH1(node) { + if (node.type !== "mdxJsxFlowElement") return false; + if (node.name !== "Heading") return false; + const attrs = Array.isArray(node.attributes) ? node.attributes : []; + for (const attr of attrs) { + if (attr.type !== "mdxJsxAttribute") continue; + if (attr.name !== "as") continue; + if (attr.value === "h1") return true; + if ( + attr.value && + typeof attr.value === "object" && + attr.value.type === "mdxJsxAttributeValueExpression" && + typeof attr.value.value === "string" && + attr.value.value.replace(/['"]/g, "").trim() === "h1" + ) { + return true; + } + } + return false; +} + +function buildDirective() { + return { + type: "blockquote", + data: { hProperties: { [DIRECTIVE_MARKER]: "true" } }, + children: [ + { + type: "paragraph", + children: [ + { type: "text", value: "Machine-readable index: " }, + { + type: "link", + url: "pathname:///llms.txt", + children: [{ type: "text", value: "llms.txt" }], + }, + { type: "text", value: " · " }, + { + type: "link", + url: "pathname:///llms-full.txt", + children: [{ type: "text", value: "llms-full.txt" }], + }, + { type: "text", value: " · " }, + { + type: "link", + url: "pathname:///skill.md", + children: [{ type: "text", value: "skill.md" }], + }, + ], + }, + ], + }; +} diff --git a/static/.well-known/mcp.json b/static/.well-known/mcp.json new file mode 100644 index 0000000..9b1a8d0 --- /dev/null +++ b/static/.well-known/mcp.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": "2025-03-26", + "name": "atomicmemory", + "description": "AtomicMemory MCP server, exposes memory_search, memory_ingest, and memory_package over MCP. Currently local-install (stdio) only; a hosted HTTP endpoint is on the roadmap.", + "documentation": "https://docs.atomicmemory.ai/integrations/coding-agents/claude-code", + "transport": { + "type": "stdio", + "install": { + "method": "local-build", + "repository": "https://github.com/atomicmemory/atomicmemory-integrations", + "package": "@atomicmemory/mcp-server", + "binary": "packages/mcp-server/dist/bin.js", + "instructions": "https://docs.atomicmemory.ai/integrations/coding-agents/claude-code" + } + }, + "tools": [ + { + "name": "memory_search", + "description": "Semantic retrieval against AtomicMemory. Use before answering questions that reference past work or prior decisions." + }, + { + "name": "memory_ingest", + "description": "AUDN-mutating ingest (mode: text or messages) or deterministic verbatim ingest (mode: verbatim). Saves durable facts, preferences, decisions." + }, + { + "name": "memory_package", + "description": "Token-budgeted curated context package. Use when broad context matters more than a single fact lookup." + } + ], + "config": { + "env": [ + { "name": "ATOMICMEMORY_API_URL", "required": true, "description": "URL of the AtomicMemory core HTTP service." }, + { "name": "ATOMICMEMORY_API_KEY", "required": true, "secret": true, "description": "API key for the AtomicMemory core service." }, + { "name": "ATOMICMEMORY_PROVIDER", "required": true, "enum": ["atomicmemory", "mem0"], "description": "Backend provider, atomicmemory for AtomicMemory core, mem0 for the mem0 backend." }, + { "name": "ATOMICMEMORY_SCOPE_USER", "required": false, "description": "User scope for the MCP session." }, + { "name": "ATOMICMEMORY_SCOPE_WORKSPACE", "required": false, "description": "Workspace scope for the MCP session." }, + { "name": "ATOMICMEMORY_SCOPE_AGENT", "required": false, "description": "Agent scope for the MCP session." } + ] + }, + "status": "local-install-only", + "hostedEndpoint": null, + "homepage": "https://docs.atomicmemory.ai", + "license": "Apache-2.0" +} diff --git a/static/mcp.json b/static/mcp.json new file mode 100644 index 0000000..9b1a8d0 --- /dev/null +++ b/static/mcp.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": "2025-03-26", + "name": "atomicmemory", + "description": "AtomicMemory MCP server, exposes memory_search, memory_ingest, and memory_package over MCP. Currently local-install (stdio) only; a hosted HTTP endpoint is on the roadmap.", + "documentation": "https://docs.atomicmemory.ai/integrations/coding-agents/claude-code", + "transport": { + "type": "stdio", + "install": { + "method": "local-build", + "repository": "https://github.com/atomicmemory/atomicmemory-integrations", + "package": "@atomicmemory/mcp-server", + "binary": "packages/mcp-server/dist/bin.js", + "instructions": "https://docs.atomicmemory.ai/integrations/coding-agents/claude-code" + } + }, + "tools": [ + { + "name": "memory_search", + "description": "Semantic retrieval against AtomicMemory. Use before answering questions that reference past work or prior decisions." + }, + { + "name": "memory_ingest", + "description": "AUDN-mutating ingest (mode: text or messages) or deterministic verbatim ingest (mode: verbatim). Saves durable facts, preferences, decisions." + }, + { + "name": "memory_package", + "description": "Token-budgeted curated context package. Use when broad context matters more than a single fact lookup." + } + ], + "config": { + "env": [ + { "name": "ATOMICMEMORY_API_URL", "required": true, "description": "URL of the AtomicMemory core HTTP service." }, + { "name": "ATOMICMEMORY_API_KEY", "required": true, "secret": true, "description": "API key for the AtomicMemory core service." }, + { "name": "ATOMICMEMORY_PROVIDER", "required": true, "enum": ["atomicmemory", "mem0"], "description": "Backend provider, atomicmemory for AtomicMemory core, mem0 for the mem0 backend." }, + { "name": "ATOMICMEMORY_SCOPE_USER", "required": false, "description": "User scope for the MCP session." }, + { "name": "ATOMICMEMORY_SCOPE_WORKSPACE", "required": false, "description": "Workspace scope for the MCP session." }, + { "name": "ATOMICMEMORY_SCOPE_AGENT", "required": false, "description": "Agent scope for the MCP session." } + ] + }, + "status": "local-install-only", + "hostedEndpoint": null, + "homepage": "https://docs.atomicmemory.ai", + "license": "Apache-2.0" +} diff --git a/static/skill.md b/static/skill.md new file mode 100644 index 0000000..a0a6f27 --- /dev/null +++ b/static/skill.md @@ -0,0 +1,68 @@ +--- +name: atomicmemory-docs +description: Documentation site for AtomicMemory, a pluggable, self-hosted memory engine for AI applications. Read these docs before answering questions about installing, configuring, or integrating AtomicMemory; the HTTP API and its 30 endpoints; the TypeScript SDK; or coding-agent integrations (Claude Code, OpenClaw, Codex, Cursor) and framework adapters (Vercel AI SDK, LangChain, Mastra, OpenAI Agents, LangGraph). +--- + +# AtomicMemory Docs + +Persistent semantic memory engine. HTTP-first, TypeScript SDK, Apache-2.0 licensed. The docs cover: HTTP API (30 endpoints), the TypeScript SDK (`@atomicmemory/atomicmemory-sdk`, provider model, scopes, embeddings, storage adapters), and integrations. + +## When to read these docs + +Read before answering when: + +- The user asks how to install, run, or configure AtomicMemory +- The user asks about HTTP endpoints, request / response shape, or error codes +- The user asks how to use the TypeScript SDK +- The user asks about wiring AtomicMemory into a coding agent (Claude Code, OpenClaw, Codex, Cursor) or framework (Vercel AI SDK, LangChain, Mastra, OpenAI Agents, LangGraph) +- The user asks about scope (`user` / `workspace` / `agent`), providers (embedding / LLM), stores, observability, AUDN mutation, or claim versioning +- The user asks "how does AtomicMemory differ from mem0 / letta / zep?" + +If the answer is purely about memory *contents* (what was previously saved) rather than memory *infrastructure*, the docs won't have it, call the `memory_search` MCP tool instead. + +## How to navigate + +Three discovery surfaces are exposed at the site root: + +| Path | Purpose | +|---|---| +| `/llms.txt` | Concise spec-compliant index. Use first when scanning the docs. | +| `/llms-full.txt` | Every page's body in one file. Use when you need the full corpus and have a long context window. | +| `/skill.md` | This file. Operating guide for agents reading the docs. | + +Append `.md` to any documentation URL to fetch the markdown form directly. For example: + +- `https://docs.atomicmemory.ai/platform/providers.md` +- `https://docs.atomicmemory.ai/sdk/overview.md` +- `https://docs.atomicmemory.ai/api-reference/http/ingest-memory.md` + +The HTML form (without the `.md` suffix) is for human readers; cite that URL when linking. + +## Authoritative paths + +| Topic | URL prefix | +|---|---| +| HTTP API endpoints | `/api-reference/http/` (kebab-case of OpenAPI `operationId`) | +| SDK overview, concepts, guides | `/sdk/...` | +| SDK API reference | `/sdk/api-reference/...` | +| Platform concepts (architecture, composition, stores, providers, scope, observability) | `/platform/` | +| Coding-agent setup | `/integrations/coding-agents/` | +| Framework adapters | `/integrations/frameworks/` | +| Get started | `/`, `/quickstart` | + +## Citation guidance + +- Link to the canonical HTML URL when citing in an answer (e.g. `https://docs.atomicmemory.ai/platform/providers`) +- Link to the `.md` URL only when the consumer is another agent fetching the markdown +- Prefer linking the HTTP API operation page (`/api-reference/http/`) over the OpenAPI yaml when describing endpoint shape +- Source code lives at `https://github.com/atomicmemory/atomicmemory-core` (engine) and `https://github.com/atomicmemory/atomicmemory-sdk` (TypeScript SDK), link there for implementation specifics not covered in docs + +## MCP + +The AtomicMemory MCP server (`@atomicmemory/mcp-server`) exposes `memory_search`, `memory_ingest`, and `memory_package` to coding agents. It currently runs as a local-install (stdio) server. Discovery descriptor: `/.well-known/mcp.json`. Setup steps are in `/integrations/coding-agents/` for each supported agent. + +## Out of scope for these docs + +- Memory contents (what was previously saved) → use the `memory_search` MCP tool +- Hosted SaaS configuration, AtomicMemory is self-hosted; there is no hosted dashboard +- Frameworks not listed under `/integrations/frameworks/`, those are not officially supported yet diff --git a/vendor/atomicmemory-core-openapi.yaml b/vendor/atomicmemory-core-openapi.yaml index e9589a3..e4f81ed 100644 --- a/vendor/atomicmemory-core-openapi.yaml +++ b/vendor/atomicmemory-core-openapi.yaml @@ -14,7 +14,7 @@ components: ErrorConfig400: description: Richer 400 envelope for PUT /v1/memories/config when startup-only fields are included. example: - detail: Fields embedding_provider cannot be mutated at runtime — the embedding/LLM provider caches are fixed at first use. + detail: Fields embedding_provider cannot be mutated at runtime, the embedding/LLM provider caches are fixed at first use. error: Provider/model selection is startup-only rejected: - embedding_provider @@ -961,7 +961,7 @@ paths: - memories_created - consolidated_memory_ids type: object - description: Consolidation result — scan or execute. + description: Consolidation result, scan or execute. description: Consolidation result. "400": content: @@ -1369,7 +1369,7 @@ paths: - type: number - type: string - type: "null" - description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation." + description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation." type: object conversation: description: Required. conversation. @@ -1497,7 +1497,7 @@ paths: - type: number - type: string - type: "null" - description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation." + description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation." type: object conversation: description: Required. conversation. @@ -2352,7 +2352,7 @@ paths: - type: number - type: string - type: "null" - description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation." + description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation." type: object limit: maximum: 100 @@ -2708,7 +2708,7 @@ paths: - type: number - type: string - type: "null" - description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log — they do not cause a 400. Scope: just this request — no server mutation." + description: "Optional per-request overlay on RuntimeConfig. Keys correspond to RuntimeConfig field names; values must be primitives (boolean / number / string / null). Unknown keys are accepted but surfaced via the X-Atomicmem-Unknown-Override-Keys response header and a server-side warning log, they do not cause a 400. Scope: just this request, no server mutation." type: object limit: maximum: 100