Skip to content

Instantly share code, notes, and snippets.

@Nipsuli
Created August 6, 2025 16:49
Show Gist options
  • Select an option

  • Save Nipsuli/f685d926c89fbcdbeb3b9622f25a36af to your computer and use it in GitHub Desktop.

Select an option

Save Nipsuli/f685d926c89fbcdbeb3b9622f25a36af to your computer and use it in GitHub Desktop.
Exploring ideas on what kind of interface would I like to have on AI memory
/**
* Exploring Idea how I'd like to interact with shared memory within my application.
* Like I want to store clear semantic Activities that user does, Users interactions
* with AI Chat and files and other content that are generated by Users.
*
* In a away combination of document RAG, LLM memory, and User beahior derived
* recommendations all in one.
*/
/**
* lazy util for documentation purposes
*/
type ISO8601datetime = string;
/**
* EntityKind describes some type of entity in the application such as:
* - user
* - workspace
* - chat
* - note
* - etc...
*/
export type EntityKind = string;
/**
* When we refer to different kinds of entities we always prefix the id with the
* EntityKind. This makes it easy to infer from any context which kind of entity
* is being referred
*/
export type EntityIdField<T extends EntityKind> = `${T}_id`;
/**
* All entity ids are strings for simplicity.
*/
export type EntityId = string;
/**
* In textual context we can refer entities by just inlining the the id with a
* prefix. Like we could add memory in format of
*
* "user_id:123 is a member in organization_id:321"
*/
export type InlineEntity<T extends EntityKind> =
`${EntityIdField<T>}:${EntityId}`;
/**
* Multiple differnt ways to refernce entity for completenes depending on the
* context.
*/
export type RichEntityId<T extends EntityKind> =
| InlineEntity<T>
| { [K in EntityIdField<T>]: EntityId }
| {
kind: T;
id: EntityId;
};
/**
* Property might contain information since when the field value has been the way
* it is. Internally each property will have since datetime, and previous values
* are maintained for history.
*/
export type PropertyWithSince = { value: string; since: ISO8601datetime };
export type Property = string | PropertyWithSince;
/**
* EntityProperties describe some direct factual things about the entity. Such
* as created_at or name or such. The map on purpose is from string to string.
* The key of the property should be descriptive enough, and if there is some
* type on the value the property key should describe it. Like using ISO8601
* datetime as created_at is quite clear for humans and LLMs. And if one needs
* to use like time stamp then the key probably should be like created_at_ms.
*
* If one needs some form of nested structure one can separate fields with .
* like:
* {
* "profile.image": ...,
* "profile.display_name": ...,
* }
*/
export type Properties = Record<string, Property>;
export type PropertiesWithSince = Record<string, PropertyWithSince>;
export type EntityBase<T extends EntityKind> = {
/**
* The id of entity need to always have the entity kind prefix to make it
* easy to identify entities without having separate discriminator field.
*/
[K in EntityIdField<T>]: EntityId;
};
export type TextContent = {
type: "text";
text: string;
};
export type FileContent = {
type: "file";
/**
* IANA media type of the file.
*
* https://www.iana.org/assignments/media-types/media-types.xhtml
*/
mediaType: string;
/**
* Optional filename of the file.
*/
filename?: string;
/**
* The URL of the file.
* It can either be a URL to a hosted file or a [Data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs).
*/
url: string;
};
export type Content = TextContent | FileContent;
/**
* Entity is the core bit of data that gets passed around. In the simplest form
* it's just the id, but it may be as rich with data as needed.
*/
export type Entity<
T extends EntityKind,
P extends Properties = Properties,
> = EntityBase<T> & {
/**
* Entity has optional properties, these can internally be used to fetch Entity
* by known field or filtering. Like entity has some time related fields such
* as created_at or updated_at those can be used for filtering recent data.
*
* In addition of just filtering these fields may be used to build as part of
* of the Entity embedding
*/
properties?: P;
/**
* Entity may contain some attached content. Example in case of Entity is like
* document it might contain text and files and such. This content should be
* analyzed and both vector and graph based representations should be build
* to be able to retrieve content.
*/
content?: Content[];
/**
* Entity may have relations. Relations are not a separate object in the API
* but nested in the entity it self.
*/
relations?: EntityRelation<EntityKind, Properties>[];
};
type EntityRelation<T extends EntityKind, P extends Properties> = {
/**
* The Entity that this is related to. In case the role
*/
entity: Entity<T>;
/**
* The relation can also have properties to map some strict factual data
*/
properties?: P;
/**
* Human (and LLM) readable explanation of the role of the relation
*/
role: string;
};
/**
* Memory is just predefined Entity with special treatment internally. When
* memory is created and it relates to some other Entities it's also attached
* to the relations of the other Entities
*/
export type Memory = Entity<
"__memory",
Properties & {
status: {
value: "processing" | "ready" | "forgotten";
since: ISO8601datetime;
};
created_at: ISO8601datetime;
}
>;
type MemoryOperationEntity<T extends EntityKind> = RichEntityId<T> | Entity<T>;
/**
* The core function for remembering things. In case there are upadates to the
* entities the entities are updated, and more recent info will override the
* data.
*/
export type RememberFn = {
/**
* Basic string based memory, things are parsed directly from the
* memory string, e.g. it might contain InlineEntity things
*/
(memory: string): Promise<Memory>;
/**
* textual memory with attached entities
*/
(
memory: string,
entities: MemoryOperationEntity<EntityKind>[],
): Promise<Memory>;
/**
* Just bulk saving some entities, either creating or updating entities
*/
(entities: MemoryOperationEntity<EntityKind>[]): Promise<Memory>;
/**
* Template literal function for utility
*/
(
memory: TemplateStringsArray,
...entities: MemoryOperationEntity<EntityKind>[]
): Promise<Memory>;
};
/**
* The core function for forgetting things.
*/
export type ForgetFn = {
/**
* Forget this fact. Text can contian InlineEntitys
*/
(memory: string): Promise<Memory>;
/**
* textual forgetting with attached entities
*/
(
memory: string,
entities: MemoryOperationEntity<EntityKind>[],
): Promise<Memory>;
/**
* bulk forgetting some entities
*/
(entities: MemoryOperationEntity<EntityKind>[]): Promise<Memory>;
/**
* Bulk forgetting some entities. If partial is true only those fields that the
* passed in entity has are forgotten. Like can be relation or property field.
*/
(
entities: MemoryOperationEntity<EntityKind>[],
partial: boolean,
): Promise<Memory>;
/**
* Template literal function for utility
*/
(
memory: TemplateStringsArray,
...entities: MemoryOperationEntity<EntityKind>[]
): Promise<Memory>;
/**
* Explicit forgetting of a memory
*/
(memory: Memory): Promise<Memory>;
};
export type RecallEntity<
T extends EntityKind,
Props extends boolean = false,
Content extends boolean = false,
Relations extends boolean = false,
> = EntityBase<T> & Props extends true
? { properties: PropertiesWithSince }
: {} & Content extends true
? { content: Content }
: {} & Relations extends true
? {
relations: {
entity: EntityBase<EntityKind> & Props extends true
? { properties: PropertiesWithSince }
: {};
role: string;
} & Props extends true
? { properties: PropertiesWithSince }
: {};
}
: {};
export type EntityGetFn = {
<T extends EntityKind>(
id: RichEntityId<T>,
): Promise<RecallEntity<T, false, false, false> | null>;
<
T extends EntityKind,
Props extends boolean,
Content extends boolean,
Relations extends boolean,
>(
id: RichEntityId<T>,
opts: {
props: Props;
content: Content;
relations: Relations;
},
): Promise<RecallEntity<T, Props, Content, Relations> | null>;
};
export type RecallResult = {
/**
* Text answer to the recall call
*/
text: string;
/**
* Minimal references to the entities that contributed to the answer
* Returned as InlineEntities[] to avoid returning objects. Caller can then
* decide to do what ever they want with the information. Like fetch more
* details about them for display purposes
*/
grounding: InlineEntity<EntityKind>[];
};
export type RecallFn = {
/**
* Basic non context generic recall
*/
(query: string): Promise<RecallResult>;
/**
* recall with some question or prompt in given context
*/
(
query: string,
context: MemoryOperationEntity<EntityKind>[],
): Promise<RecallResult>;
/**
* recall within some context. Example can be used to call when user opens a
* document to get some memory what was going on last time they were there
*/
(query: MemoryOperationEntity<EntityKind>[]): Promise<RecallResult>;
/**
* Template literal function for utility
*/
(
query: TemplateStringsArray,
...context: MemoryOperationEntity<EntityKind>[]
): Promise<RecallResult>;
};
/**
* The super mega powerful memory client.
*/
export type MemoryClient = {
remember: RememberFn;
forget: ForgetFn;
recall: RecallFn;
/**
* Utility for pulling the exact entity out
*/
getEntity: EntityGetFn;
};
export type CreateMemoryClientFn = (opts: {
apiKey: string;
/**
* Each namespace is isolated and within namespace one has Access to everything.
* There should not be any need to manually create or manage namespaces. Similarly
* as there should not be any need to manage which entities can and cannot be there.
* just calling with new namespace or entity kind just does needed things.
*/
namespace: string;
}) => MemoryClient;
//
// USAGE
//
declare const memoryClient: MemoryClient;
// Create some memories
// just text memory with InlineEntityId
const memory = await memoryClient.remember("user_id:123 likes bananas");
// product event type of memory
await memoryClient.remember("User joined orgazation", [
{
user_id: "123",
properties: { username: "Nipsuli", nickname: "The Data Cowboy" },
},
// alternative way to flag contextual entity
{ kind: "organization", id: "321" },
]);
// remember some object
await memoryClient.remember([
{
note_id: "abc",
content: [
{
type: "text",
text: "Some note...",
},
],
relations: [
{
entity: { user_id: "123" },
role: "Created by",
},
],
},
]);
// with tag literal
const user = { user_id: "123" };
const chatMessage = {
chat_message_id: "def",
properties: {
chat_message_role: "user",
},
relations: [
{
chat_id: "qwe",
role: "Belongs to",
},
],
};
await memoryClient.remember`${user} sent ${chatMessage}`;
// Forgetting things
// explicit forgetting
await memoryClient.forget(memory);
// fuzzy forgetting
await memoryClient.forget("Evertying that happened in Vegas");
// forgetting in context
await memoryClient.forget("All the things user has done", [{ user_id: "123" }]);
// forgetting these entities ever existed
await memoryClient.forget([
// InlineEntity works
"note_id:abc",
// as with other formats for entity
{ file_id: "xyz" },
]);
// partial forgetting
await memoryClient.forget(
[
{
user_id: "123",
properties: {
// the value doesn't matter if we're forgetting property
nickname: "",
},
},
],
// partial forgetting: will only forget properties and relations that
// are passed in
true,
);
// template literal
const user2 = "user_id:123";
const movie = {
movie_id: "Human centipede",
};
await memoryClient.forget`${user2} saw ${movie}`;
// Recalling
// Generic recall
await memoryClient.recall(
"What was the pricing that we originally thought for this service?",
);
// Contextual recall with query
await memoryClient.recall("What have I done during this week?", [
{ user_id: "123" },
]);
// Generic contextual recall
await memoryClient.recall([
// again both InlineEntity
"note_id:abc",
// And other Entity formats work
{ user_id: "123" },
]);
// And templates
const recallRes =
await memoryClient.recall`What did ${user2} think about ${movie}?`;
console.log(recallRes.text); // Can't remember Nipsuli ever seeing movie Human centipede
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment