feat(profiles): user-only SDK for v1 (drop regenerate + agent)

Reduce the User Profiles SDK to the shipped v1 scope: user profiles only,
no full rebuild.

- Remove regenerate_profiles / regenerateProfiles (sync + async, Python + TS)
  and the ProfileRegenerateResponse type. Full rebuild is gated off server-side
  (409 not_yet_available).
- Drop the entity_type / entityType parameter from get_profile,
  generate_profile, sample_profiles, and update_profile_settings; every path
  and payload is scoped to "user". Narrow ProfileEntityType to "user".
- Update tests to the user-only surface.

Endpoint paths (/v2/profiles/jobs/, /v2/profiles/settings/,
/v2/entities/user/{id}/profile/) and the per-job Idempotency-Key are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
karthik
2026-09-23 19:50:48 +05:30
co-authored by Claude Opus 4.8
parent 949b026991
commit 48ae36ad94
6 changed files with 93 additions and 297 deletions
-1
View File
@@ -32,7 +32,6 @@ export type {
EntityProfileSettings,
ProfileSampleResult,
ProfileSamplesResponse,
ProfileRegenerateResponse,
} from "./mem0.types";
// Re-export enums as values (not type-only)
+15 -56
View File
@@ -28,7 +28,6 @@ import {
ProfileSettings,
ProfileSettingsResponse,
ProfileSamplesResponse,
ProfileRegenerateResponse,
} from "./mem0.types";
import {
captureClientEvent,
@@ -772,21 +771,17 @@ export default class MemoryClient {
}
/**
* Get the memory profile for a single entity.
* Get the memory profile for a single user.
*
* Branch on `status`, not on an empty `profile`: generation is asynchronous,
* so a known entity without a profile yet is a normal response.
* so a known user without a profile yet is a normal response.
*/
async getProfile(data: {
entityId: string;
entityType?: ProfileEntityType;
}): Promise<ProfileResponse> {
async getProfile(data: { entityId: string }): Promise<ProfileResponse> {
this._captureEvent("get_profile", []);
await this._awaitIdentity();
const entityType = data.entityType ?? "user";
const response = await this._fetchWithErrorHandling(
`${this.host}/v2/entities/${encodeURIComponent(entityType)}/${encodeURIComponent(data.entityId)}/profile/`,
`${this.host}/v2/entities/user/${encodeURIComponent(data.entityId)}/profile/`,
{
headers: this.headers,
},
@@ -795,15 +790,14 @@ export default class MemoryClient {
}
/**
* Generate or refresh the profile for one entity, now.
* Generate or refresh the profile for one user, now.
*
* Profiles are otherwise built once an entity crosses an internal message
* threshold, so a new entity has none for its first few memories. Returns as
* Profiles are otherwise built once a user crosses an internal message
* threshold, so a new user has none for its first few memories. Returns as
* soon as the work is queued: poll {@link getProfile} and branch on `status`.
*/
async generateProfile(data: {
entityId: string;
entityType?: ProfileEntityType;
}): Promise<ProfileTriggerResponse> {
this._captureEvent("generate_profile", []);
await this._awaitIdentity();
@@ -815,7 +809,7 @@ export default class MemoryClient {
headers: { ...this.headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
operation: "trigger",
entity_type: data.entityType ?? "user",
entity_type: "user",
entity_id: data.entityId,
}),
},
@@ -873,7 +867,7 @@ export default class MemoryClient {
/**
* Update profile settings. Only the fields you pass are written.
*
* `schema` and `customInstructions` are per entity type and are nested under
* `schema` and `customInstructions` are per user and are nested under
* `entities` for the API; only `enabled` is project-wide. Sending them flat
* is rejected with `Unsupported settings`.
*/
@@ -884,12 +878,7 @@ export default class MemoryClient {
this._captureEvent("update_profile_settings", [payloadKeys]);
await this._awaitIdentity();
const {
schema,
customInstructions,
enabled,
entityType = "user",
} = settings || {};
const { schema, customInstructions, enabled } = settings || {};
const body: Record<string, any> = {};
if (enabled !== undefined) {
@@ -905,7 +894,7 @@ export default class MemoryClient {
entitySettings.custom_instructions = customInstructions;
}
if (Object.keys(entitySettings).length > 0) {
body.entities = { [entityType]: entitySettings };
body.entities = { user: entitySettings };
}
const raw = await this._fetchRawJson(`${this.host}/v2/profiles/settings/`, {
@@ -917,15 +906,12 @@ export default class MemoryClient {
}
/**
* Generate profiles for a few real entities, to check a schema.
* Generate profiles for a few real users, to check a schema.
*
* Real generations against real memories, and the results are kept: the
* profiles are written to those entities and count toward usage.
* profiles are written to those users and count toward usage.
*/
async sampleProfiles(data?: {
limit?: number;
entityType?: ProfileEntityType;
}): Promise<ProfileJobResponse> {
async sampleProfiles(data?: { limit?: number }): Promise<ProfileJobResponse> {
this._captureEvent("sample_profiles", []);
await this._awaitIdentity();
@@ -937,7 +923,7 @@ export default class MemoryClient {
body: JSON.stringify({
operation: "sample",
// Required: the API refuses a job that does not name an entity kind.
entity_type: data?.entityType ?? "user",
entity_type: "user",
...this._prepareParams({ limit: data?.limit }),
}),
},
@@ -945,33 +931,6 @@ export default class MemoryClient {
return response;
}
/**
* Rebuild the profile of every entity of one kind in the project.
*
* Not available yet: the server answers 409 `not_yet_available` and creates
* nothing. Use {@link sampleProfiles} or {@link generateProfile} until
* `capabilities.full_rebuild` from {@link getProfileSettings} is true.
*/
async regenerateProfiles(data?: {
entityType?: ProfileEntityType;
}): Promise<ProfileJobResponse> {
this._captureEvent("regenerate_profiles", []);
await this._awaitIdentity();
const response = await this._fetchWithErrorHandling(
`${this.host}${PROFILE_JOBS_PATH}`,
{
method: "POST",
headers: { ...this.headers, "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({
operation: "regenerate",
entity_type: data?.entityType ?? "user",
}),
},
);
return response;
}
/**
* Read one generation job. Accepts the `statusUrl` from a create call, or a
* bare job id. Prefer `statusUrl` so a route change needs no client update.
+8 -9
View File
@@ -239,8 +239,8 @@ export interface GetMemoryExportPayload {
// ─── Profile Types ──────────────────────────────────────────
/** Entity kinds that can carry a profile. */
export type ProfileEntityType = "user" | "agent";
/** The entity kind that carries a profile. */
export type ProfileEntityType = "user";
/**
* `succeeded` is the only state in which `profile` is guaranteed to hold content.
@@ -249,7 +249,11 @@ export type ProfileEntityType = "user" | "agent";
* spelling is what a comparison has to match.
*/
export type ProfileStatus =
"succeeded" | "pending" | "failed" | "not_enabled" | "insufficient_data";
| "succeeded"
| "pending"
| "failed"
| "not_enabled"
| "insufficient_data";
export interface ProfileResponse {
/** Shaped by the project's schema; keys are not camel-cased. */
@@ -282,15 +286,13 @@ export interface ProfileJobResponse {
/** @deprecated Use {@link ProfileJobResponse}. */
export type ProfileTriggerResponse = ProfileJobResponse;
/** The settings to write. `schema` and `customInstructions` apply to one entity type. */
/** The settings to write. `schema` and `customInstructions` apply to user profiles. */
export interface ProfileSettings {
/** Turn profile generation on or off. Project-wide. */
enabled?: boolean;
/** JSON Schema for the profile. Every property needs a `description`. */
schema?: Record<string, any> | null;
customInstructions?: string | null;
/** Which entity kind `schema` and `customInstructions` belong to. Defaults to "user". */
entityType?: ProfileEntityType;
}
/** One entity type's stored configuration. */
@@ -322,9 +324,6 @@ export interface ProfileSampleResult {
/** @deprecated Use {@link ProfileJobResponse}. */
export type ProfileSamplesResponse = ProfileJobResponse;
/** @deprecated Use {@link ProfileJobResponse}. */
export type ProfileRegenerateResponse = ProfileJobResponse;
/** `GET /v2/profiles/jobs/{id}/`. The job nests under `job`. */
export interface ProfileJobStatus {
job: {
@@ -76,18 +76,18 @@ describe("MemoryClient - getProfile()", () => {
expect(status).not.toBe("insufficientData");
});
test("defaults to user and encodes the entity id", async () => {
test("scopes to user and encodes the entity id", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v2/entities/agent/", {
extra.set("/v2/entities/user/", {
status: 200,
body: { profile: {}, status: "pending", entity_type: "agent" },
body: { profile: {}, status: "pending", entity_type: "user" },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.getProfile({ entityId: "a/b", entityType: "agent" });
await client.getProfile({ entityId: "a/b" });
const call = findFetchCall(mock, "/v2/entities/agent/a%2Fb/profile/");
const call = findFetchCall(mock, "/v2/entities/user/a%2Fb/profile/");
expect(call).toBeDefined();
});
});
@@ -179,7 +179,7 @@ describe("MemoryClient - profile settings", () => {
);
});
test("targets the entity type the caller named", async () => {
test("scopes entity-level settings under user", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v2/profiles/settings/", {
status: 200,
@@ -190,13 +190,12 @@ describe("MemoryClient - profile settings", () => {
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.updateProfileSettings({
schema: { type: "object", properties: {} },
entityType: "agent",
});
const body = getFetchBody(
findFetchCall(mock, "/v2/profiles/settings/", "POST")!,
);
expect(Object.keys(body.entities)).toEqual(["agent"]);
expect(Object.keys(body.entities)).toEqual(["user"]);
});
test("omits fields the caller did not set", async () => {
@@ -251,7 +250,7 @@ describe("MemoryClient - profile settings", () => {
});
});
describe("MemoryClient - sampleProfiles() / regenerateProfiles()", () => {
describe("MemoryClient - sampleProfiles()", () => {
test("sampleProfiles omits limit when unset", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v2/profiles/jobs/", {
@@ -304,46 +303,4 @@ describe("MemoryClient - sampleProfiles() / regenerateProfiles()", () => {
expect(getFetchBody(call!).entity_type).toBe("user");
expect(result.sampled).toBe(3);
});
test("sampleProfiles targets the entity type the caller named", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v2/profiles/jobs/", {
status: 202,
body: { job_id: "job_4", status: "QUEUED", entity_type: "agent" },
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
await client.sampleProfiles({ entityType: "agent" });
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
expect(getFetchBody(call!).entity_type).toBe("agent");
});
test("sends operation regenerate to the jobs collection", async () => {
const extra = new Map<string, { status: number; body: unknown }>();
extra.set("/v2/profiles/jobs/", {
status: 202,
body: {
job_id: "job_3",
status: "QUEUED",
status_url: "/v2/profiles/jobs/job_3/",
operation: "regenerate",
entity_type: "user",
},
});
const mock = setupMockFetch(extra);
const client = new MemoryClient({ apiKey: TEST_API_KEY });
const result = await client.regenerateProfiles();
const call = findFetchCall(mock, "/v2/profiles/jobs/", "POST");
expect(call).toBeDefined();
expect(getFetchBody(call!)).toEqual({
operation: "regenerate",
entity_type: "user",
});
expect(result.jobId).toBe("job_3");
expect(result.statusUrl).toBe("/v2/profiles/jobs/job_3/");
});
});