diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 49e4192f..4b5f0c1d 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -6,6 +6,9 @@ export default { title: "EverMemoryArchive", description: "EverMemoryArchive is a platform for creating and managing memory-based agents.", + head: [ + ...mermaidHead() + ], themeConfig: { sidebar: [ // overview @@ -50,6 +53,41 @@ export default { ] }; +function mermaidHead() { + return [ + [ + 'script', + { + id: 'mermaid-js', + src: 'https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js', + async: true, + } + ], + [ + 'script', + {}, + `;(() => { +const mermaidJS = document.getElementById('mermaid-js'); +document.addEventListener('DOMContentLoaded', async () => { + await mermaidJS.ready; + const isDark = document.documentElement.classList.contains('dark'); + mermaid.initialize({ startOnLoad: false, theme: isDark ? 'dark' : 'neutral' }); + const mermaidElements = document.getElementsByClassName('language-mermaid'); + for (const code of mermaidElements) { + const preCode = code.querySelector('pre code'); + if (preCode) { + const codeText = preCode.textContent; + const randomId = "mermaid-" + Math.random().toString(36).substring(2, 15); + const {svg} = await mermaid.render(randomId, codeText); + code.innerHTML = svg; + } + } +}); +})()` + ] + ]; +} + function flatHttpSidebar(sidebar) { const routes = []; walk([], sidebar); diff --git a/packages/ema/src/actor.ts b/packages/ema/src/actor.ts index 127e7c0b..2e410d45 100644 --- a/packages/ema/src/actor.ts +++ b/packages/ema/src/actor.ts @@ -23,7 +23,12 @@ export interface ActorScope { } /** - * A facade of the actor functionalities between the server (system) and the agent (actor). + * A facade of actor runtime behavior between server and agent. + * + * Note: + * - Current production runtime (memory branch) uses queued inputs, interruptible + * runs, and conversation-scoped memory injection. + * - This branch keeps a simplified compatibility implementation for reference. */ export class ActorWorker { /** Event emitter for actor events. */ diff --git a/packages/ema/src/concept/actor.ts b/packages/ema/src/concept/actor.ts new file mode 100644 index 00000000..8f48c330 --- /dev/null +++ b/packages/ema/src/concept/actor.ts @@ -0,0 +1,132 @@ +import type { EventEmitter } from "node:events"; +import type { Content as InputContent } from "../schema"; +import type { AgentEventName, AgentEvent, AgentEventUnion } from "./llm"; + +/** + * The scope information for the actor. + */ +export interface ActorScope { + actorId: number; + userId: number; + conversationId: number; +} + +/** + * A batch of actor inputs in one request. + */ +export type ActorInputs = InputContent[]; + +/** + * The status of the actor. + */ +export type ActorStatus = "preparing" | "running" | "idle"; + +/** + * A message from the actor. + */ +export interface ActorMessageEvent { + /** The kind of the event. */ + kind: "message"; + /** The content of the message. */ + content: string; +} + +/** + * An event forwarded from the agent. + */ +export interface ActorAgentEvent { + /** The kind of the event. */ + kind: AgentEventName; + /** The content of the message. */ + content: AgentEventUnion; +} + +/** + * The event map for actor events. + */ +export interface ActorEventMap { + message: [ActorMessageEvent]; + agent: [ActorAgentEvent]; +} + +/** + * Union of actor event names. + */ +export type ActorEventName = keyof ActorEventMap; + +/** + * Type mapping of actor event names to their corresponding event data types. + */ +export type ActorEvent = ActorEventMap[K][0]; + +/** + * Union type of all actor event contents. + */ +export type ActorEventUnion = ActorEvent; + +/** + * Constant mapping of actor event names for iteration. + */ +export const ActorEventNames: Record = { + message: "message", + agent: "agent", +}; + +/** + * Event source interface for the actor. + */ +export interface ActorEventSource { + on( + event: K, + handler: (content: ActorEvent) => void, + ): this; + off( + event: K, + handler: (content: ActorEvent) => void, + ): this; + once( + event: K, + handler: (content: ActorEvent) => void, + ): this; + emit(event: K, content: ActorEvent): boolean; +} + +/** + * Typed event emitter for actor events. + */ +export type ActorEventsEmitter = EventEmitter & ActorEventSource; + +/** + * Type guard that narrows an actor event to a specific agent event (or any agent event). + */ +export function isAgentEvent( + event: ActorEventUnion, + kind?: K, +): event is ActorAgentEvent & + (K extends AgentEventName + ? { kind: K; content: AgentEvent } + : ActorAgentEvent) { + if (!event) return false; + if (event.kind === "message") return false; + return kind ? event.kind === kind : true; +} + +/** + * A facade of the actor functionalities between the server (system) and the agent (actor). + */ +export declare class ActorWorker { + /** + * Event emitter for actor events. + */ + readonly events: ActorEventsEmitter; + /** + * Enqueues inputs and runs the agent sequentially for this actor. + * @param inputs - Batch of user inputs for a single request. + * @param addToBuffer - Whether to persist inputs to conversation buffer. + */ + work(inputs: ActorInputs, addToBuffer?: boolean): Promise; + /** + * Reports whether the actor is currently preparing or running. + */ + isBusy(): boolean; +} diff --git a/packages/ema/src/concept/compat.ts b/packages/ema/src/concept/compat.ts new file mode 100644 index 00000000..e7073f07 --- /dev/null +++ b/packages/ema/src/concept/compat.ts @@ -0,0 +1,41 @@ +import type { Message } from "../schema"; + +/** + * Compatibility-only types for legacy actor implementation in this branch. + * These are intentionally not exported from `concept/index.ts`. + */ + +export interface ActorStateStorage { + getState(): Promise; + updateState(state: ActorState): Promise; +} + +export interface ActorState { + memoryBuffer: Message[]; +} + +export interface ActorMemory { + search(keywords: string[]): Promise; + addShortTermMemory(item: ShortTermMemory): Promise; + addLongTermMemory(item: LongTermMemory): Promise; +} + +export interface SearchActorMemoryResult { + items: LongTermMemory[]; +} + +export interface ShortTermMemory { + kind: "year" | "month" | "day"; + os: string; + statement: string; + createdAt: number; +} + +export interface LongTermMemory { + index0: string; + index1: string; + keywords: string[]; + os: string; + statement: string; + createdAt: number; +} diff --git a/packages/ema/src/concept/index.ts b/packages/ema/src/concept/index.ts new file mode 100644 index 00000000..f99f3bea --- /dev/null +++ b/packages/ema/src/concept/index.ts @@ -0,0 +1,52 @@ +/** + * Conceptual architecture of EverMemoryArchive (aligned with memory runtime design). + * + * High-level flow: + * 1. UI sends user input to server APIs. + * 2. Server resolves `(user, actor, conversation)` and routes to an `ActorWorker`. + * 3. `ActorWorker` serializes buffer writes, queues inputs, and may interrupt an in-flight run. + * 4. `Agent` executes an LLM+tool loop and emits structured events. + * 5. `MemoryManager` persists conversation buffer and memory records, then injects memory into prompts. + * 6. `Scheduler` drives foreground/background jobs (for reminders and memory organization). + * + * ```mermaid + * graph TD + * subgraph UI["UI Layer"] + * WebUI["Web UI"] + * Clients["Other Clients"] + * end + * + * subgraph Runtime["EMA Runtime"] + * Server["Server"] + * Actor["ActorWorker"] + * Agent["Agent (LLM + Tools Loop)"] + * Memory["MemoryManager"] + * Scheduler["Scheduler"] + * end + * + * subgraph Storage["Storage Layer"] + * Mongo["MongoDB"] + * Lance["LanceDB (Vector Search)"] + * end + * + * subgraph LLM["LLM Providers"] + * OpenAI["OpenAI-Compatible"] + * Google["Google GenAI"] + * end + * + * UI --> Server + * Server --> Actor + * Actor --> Agent + * Agent --> LLM + * Actor --> Memory + * Memory --> Storage + * Server --> Scheduler + * Scheduler --> Actor + * ``` + * + * @module @internals/concept + */ + +export * from "./actor"; +export * from "./llm"; +export * from "./storage"; diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts new file mode 100644 index 00000000..cb01dcfa --- /dev/null +++ b/packages/ema/src/concept/llm.ts @@ -0,0 +1,281 @@ +import type { EventEmitter } from "node:events"; +import type { Message, LLMResponse } from "../schema"; +import type { Tool } from "../tools/base"; +import type { EmaReply } from "../tools/ema_reply_tool"; + +/** + * LLM providers supported by EMA runtime. + */ +export enum LLMProvider { + GOOGLE = "google", + ANTHROPIC = "anthropic", + OPENAI = "openai", +} + +/** + * Stateless LLM client abstraction used by agent runtime. + */ +export declare class LLMClient { + /** + * Generates one model turn from current context. + * + * @param messages - Conversation history in EMA schema. + * @param tools - Optional tool definitions. + * @param systemPrompt - Optional system prompt for this call. + * @param signal - Optional abort signal. + */ + generate( + messages: Message[], + tools?: Tool[], + systemPrompt?: string, + signal?: AbortSignal, + ): Promise; +} + +/** Event emitted when the agent finishes a run. */ +export interface RunFinishedEvent { + ok: boolean; + msg: string; + error?: Error; +} + +/* Emitted when the ema_reply tool is called successfully. */ +export interface EmaReplyReceivedEvent { + reply: EmaReply; +} + +/** Map of agent event names to their corresponding event data types. */ +export interface AgentEventMap { + runFinished: [RunFinishedEvent]; + emaReplyReceived: [EmaReplyReceivedEvent]; +} + +/** Union type of all agent event names. */ +export type AgentEventName = keyof AgentEventMap; + +/** Type mapping of agent event names to their corresponding event data types. */ +export type AgentEvent = AgentEventMap[K][0]; + +/** Union type of all agent event contents. */ +export type AgentEventUnion = AgentEvent; + +/** Constant mapping of agent event names for iteration. */ +export const AgentEventNames: Record = { + runFinished: "runFinished", + emaReplyReceived: "emaReplyReceived", +}; + +/** Event source interface for the agent. */ +export interface AgentEventSource { + on( + event: K, + handler: (content: AgentEvent) => void, + ): this; + off( + event: K, + handler: (content: AgentEvent) => void, + ): this; + once( + event: K, + handler: (content: AgentEvent) => void, + ): this; + emit(event: K, content: AgentEvent): boolean; +} + +export type AgentEventsEmitter = EventEmitter & AgentEventSource; + +/** The runtime state of the agent. */ +export type AgentState = { + systemPrompt: string; + messages: Message[]; + tools: Tool[]; + toolContext?: unknown; +}; + +/** Callback type for running the agent with a given state. */ +export type AgentStateCallback = ( + next: (state: AgentState) => Promise, +) => Promise; + +/** Agent abstraction (event-driven LLM + tool execution loop). */ +export declare class Agent { + events: AgentEventsEmitter; + isRunning(): boolean; + abort(): Promise; + runWithState(state: AgentState): Promise; + run(callback: AgentStateCallback): Promise; +} + +/** + * Mapping of job names to payload shape. + */ +export type JobDataMap = Record>; + +/** + * Union of all job names. + */ +export type JobName = keyof JobDataMap & string; + +/** + * Data type for a specific job name. + * @typeParam K - The job name. + */ +export type JobData = JobDataMap[K]; + +/** + * Union of all job data types. + */ +export type JobDataUnion = JobData; + +/** + * Scheduler job shape. + */ +export type Job = { + attrs: { + name: K; + data: JobData; + }; +}; + +/** + * Scheduler job identifier. + */ +export type JobId = string; + +/** + * Input data for scheduling a job. + */ +export interface JobSpec { + /** + * The job name used to resolve a handler. + */ + name: K; + /** + * When the job should run (Unix timestamp in milliseconds). + */ + runAt: number; + /** + * Handler-specific data. + */ + data: JobData; +} + +/** + * Input data for scheduling a recurring job. + */ +export interface JobEverySpec { + /** + * The job name used to resolve a handler. + */ + name: K; + /** + * Earliest time the recurring schedule becomes active (Unix timestamp in milliseconds). + */ + runAt: number; + /** + * How often the job should repeat (Agenda interval string or milliseconds). + */ + interval: string | number; + /** + * Handler-specific data. + */ + data: JobData; + /** + * Uniqueness criteria for deduplicating recurring jobs. + */ + unique?: Record; +} + +/** + * Scheduler job handler signature. + */ +export type JobHandler = ( + job: Job, + done?: (error?: Error) => void, +) => Promise | void; + +/** + * Type guard to narrow a job to a specific name/data pair. + * @param job - The job instance to check. + * @param name - The expected job name. + * @returns True when the job matches the provided name. + */ +export function isJob( + job: Job | null | undefined, + name: K, +): job is Job { + return !!job && job.attrs.name === name; +} + +/** + * Scheduler interface for managing job lifecycle. + */ +export interface Scheduler { + /** + * Starts the scheduler loop. + * @param handlers - Mapping of job names to their handlers. + * @returns Promise resolving when the scheduler is started. + */ + start(handlers: JobHandlerMap): Promise; + /** + * Stops the scheduler loop. + * @returns Promise resolving when the scheduler is stopped. + */ + stop(): Promise; + /** + * Schedules a job for execution. + * @param job - The job to schedule. + * @returns Promise resolving to the job id. + */ + schedule(job: JobSpec): Promise; + /** + * Reschedules an existing queued job with new runAt/data. + * @param id - The job identifier. + * @param job - The new job data. + * @returns Promise resolving to true if rescheduled, false otherwise. + */ + reschedule(id: JobId, job: JobSpec): Promise; + /** + * Cancels a pending job by id. + * @param id - The job identifier. + * @returns Promise resolving to true if canceled, false otherwise. + */ + cancel(id: JobId): Promise; + /** + * Schedules a recurring job. + * @param job - The recurring job data. + * @returns Promise resolving to the job id. + */ + scheduleEvery(job: JobEverySpec): Promise; + /** + * Reschedules an existing recurring job. + * @param id - The job identifier. + * @param job - The new recurring job data. + * @returns Promise resolving to true if rescheduled, false otherwise. + */ + rescheduleEvery(id: JobId, job: JobEverySpec): Promise; + /** + * Gets a job by id. + * @param id - The job identifier. + * @returns Promise resolving to the job if found. + */ + getJob(id: JobId): Promise; + /** + * Lists jobs with an optional filter. + * @param filter - Filter for jobs. + * @returns Promise resolving to matching jobs. + */ + listJobs(filter?: Record): Promise; +} + +/** + * Mapping of job names to their handlers. + */ +export type JobHandlerMap = Partial<{ + [K in JobName]: JobHandler; +}>; + +/** + * Runtime status of the scheduler. + */ +export type SchedulerStatus = "idle" | "running" | "stopping"; diff --git a/packages/ema/src/concept/storage.ts b/packages/ema/src/concept/storage.ts new file mode 100644 index 00000000..ba1b2482 --- /dev/null +++ b/packages/ema/src/concept/storage.ts @@ -0,0 +1,194 @@ +import type { Content as InputContent } from "../schema"; + +/** + * Represents a persisted message with metadata for buffer history. + */ +export interface BufferMessage { + /** + * The role that produced the message. + */ + kind: "user" | "actor"; + /** + * The identifier of the message author (userId / actorId). + */ + role_id: number; + /** + * The unique identifier of the persisted message. + * May be absent before the message is stored. + */ + msg_id?: number; + /** + * The message contents. + */ + contents: InputContent[]; + /** + * The time the message was recorded (Unix timestamp in milliseconds). + */ + time: number; +} + +/** + * Interface for persisting and reading buffer messages. + */ +export interface BufferStorage { + /** + * Gets buffer messages. + * @param conversationId - The conversation identifier to read. + * @param count - The number of messages to return. + * @returns Promise resolving to the buffer messages. + */ + getBuffer(conversationId: number, count: number): Promise; + /** + * Adds a buffer message. + * @param conversationId - The conversation identifier to write. + * @param message - The buffer message to add. + * @returns Promise resolving when the message is stored. + */ + addBuffer(conversationId: number, message: BufferMessage): Promise; +} + +/** + * Interface for persisting actor state. + */ +export interface ActorStateStorage { + /** + * Gets the state of the actor + * @param actorId - The actor identifier to read. + * @param conversationId - The conversation identifier to read. + * @returns Promise resolving to the state of the actor + */ + getState(actorId: number, conversationId: number): Promise; +} + +/** + * Runtime state for an actor. + */ +export interface ActorState { + /** + * The lastest short-term memory for the actor. + */ + memoryDay: ShortTermMemory; + memoryWeek: ShortTermMemory; + memoryMonth: ShortTermMemory; + memoryYear: ShortTermMemory; + /** + * The buffer messages for the actor. + */ + buffer: BufferMessage[]; +} + +/** + * Interface for actor memory. + */ +export interface ActorMemory { + /** + * Searches actor memory + * @param actorId - The actor identifier to search. + * @param memory - The memory text to search against. + * @param limit - Maximum number of memories to return. + * @param index0 - Optional index0 filter. + * @param index1 - Optional index1 filter. + * @returns Promise resolving to the search result + */ + search( + actorId: number, + memory: string, + limit: number, + index0?: string, + index1?: string, + ): Promise; + /** + * Lists short term memories for the actor + * @param actorId - The actor identifier to query. + * @param kind - Optional memory kind filter. + * @param limit - Optional maximum number of memories to return. + * @returns Promise resolving to short term memory records sorted by newest first. + */ + getShortTermMemory( + actorId: number, + kind?: ShortTermMemory["kind"], + limit?: number, + ): Promise; + /** + * Adds short term memory + * @param actorId - The actor identifier to update. + * @param item - Short term memory item + * @returns Promise resolving when the memory is added + */ + addShortTermMemory(actorId: number, item: ShortTermMemory): Promise; + /** + * Adds long term memory + * @param actorId - The actor identifier to update. + * @param item - Long term memory item + * @returns Promise resolving when the memory is added + */ + addLongTermMemory(actorId: number, item: LongTermMemory): Promise; +} + +/** + * Short-term memory item captured at a specific granularity. + */ +export interface ShortTermMemory { + /** + * The granularity of short term memory. + */ + kind: "year" | "month" | "week" | "day"; + /** + * The memory text when the actor saw the messages. + */ + memory: string; + /** + * The date and time the memory was created. + */ + createdAt?: number; + /** + * Related conversation message IDs for traceability. + */ + messages?: number[]; +} + +/** + * Short-term memory record with identifier. + */ +export type ShortTermMemoryRecord = ShortTermMemory & { + /** + * The unique identifier for the memory record. + */ + id: number; +}; + +/** + * Long-term memory item used for retrieval. + */ +export interface LongTermMemory { + /** + * The 0-index to search, a.k.a. 一级分类 + */ + index0: string; + /** + * The 1-index to search, a.k.a. 二级分类 + */ + index1: string; + /** + * The memory text when the actor saw the messages. + */ + memory: string; + /** + * The date and time the memory was created + */ + createdAt?: number; + /** + * Related conversation message IDs for traceability. + */ + messages?: number[]; +} + +/** + * Long-term memory record with identifier. + */ +export type LongTermMemoryRecord = LongTermMemory & { + /** + * The unique identifier for the memory record. + */ + id: number; +}; diff --git a/packages/ema/src/concept/task.ts b/packages/ema/src/concept/task.ts new file mode 100644 index 00000000..3d38bd5c --- /dev/null +++ b/packages/ema/src/concept/task.ts @@ -0,0 +1,250 @@ +import type { Agent } from "./llm"; + +/** + * A task is a unit of work that can be scheduled and run. + */ +export interface Task { + /** + * A human-readable name of the task. + */ + name: string; + + /** + * An interface to format task in debug consoles. + * If not provided, the task will be formatted as `[Task: ]` in debug consoles. + */ + describe?(): string; +} + +/** + * An agent task is a task that runs with an agent. + * - For a timed agent task, use {@link TimedTaskScheduler.iterateTimed}. + * + * @example + * ```ts + * // Runs an agent task every day at midnight forever. + * const dailyTask: CronTask & AgentTask = { + * name: "daily-task", + * cron: "0 0 * * *", + * async work(agent) { + * for await (const date of scheduler.iterateTimed(this)) { + * await agent.runWithMessage({ type: "user", content: `Today is ${date}`}); + * } + * }, + * }; + * scheduler.schedule(dailyTask); + * ``` + */ +export interface AgentTask extends Task { + /** + * The agent to run the task with. + * If not provided, the task will run with a new agent. + */ + agent?: Agent; + + /** + * Runs the task with the agent and schedule context. + * + * @param agent - The agent to run the task with. *Note that the agent may be running when it is scheduled.* + * @param scheduler - The scheduler to run the task with. + * @returns Promise resolving when the task is completed. + */ + work(agent: Agent, scheduler: AgentTaskScheduler): Promise; +} + +/** + * The scheduler of the agent. A scheduler manages multiple llm sessions established by agents with a sensible resource + * limits. + */ +export interface AgentTaskScheduler { + /** + * Schedules a task to run. + * + * @param task - The task to schedule. + * @returns Promise resolving when the task is scheduled. + */ + schedule(task: AgentTask): Promise; + /** + * Waits for the agent to be idle. + * + * @param agent - The agent to wait for. + * @param timeout - The timeout in milliseconds. If not provided, the agent will wait indefinitely. + * @returns Promise resolving when the agent is idle or the timeout is reached. + */ + waitForIdle(agent: Agent, timeout?: number): Promise; +} + +/** + * A timed task is a descriptor of a function that runs deferred or periodically. + * ``` + */ +export type TimedTask = CronTask | TickTask; + +/** + * A descriptor to run a timed task according to a cron expression. + * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. + * - Use {@link https://crontab.guru/} to create cron expressions. + * + * @example + * ```ts + * // Runs a cron task every day at midnight forever. + * const cronTask: CronTask = { + * name: "daily-task", + * cron: "0 0 * * *", + * }; + * scheduler.startTimed(cronTask, (date) => { + * console.log(`Today is ${date}`); + * }); + * ``` + */ +export interface CronTask extends Task { + /** + * A cron expression of the task. + */ + cron: string; + /** + * Whether the task should run only once. + */ + once?: boolean; +} + +export interface TickTask extends Task { + /** + * A tick interval in milliseconds. + */ + tick: number; + /** + * Whether the task should run only once. + */ + once?: boolean; +} + +/** + * A table to control a timed task. + */ +interface TimedTab { + /** + * Whether the timed task is cancelled. + */ + cancelled: boolean; + /** + * Cancels the timed task. This function can be called multiple times safely. + */ + cancel(): void; +} + +/** + * The scheduler of the cron task. + */ +export abstract class TimedTaskScheduler { + /** + * Starts a timed task to run. + * + * @param task - The timed task to schedule. + * @param cb - The callback to run the timed task. + * @returns A table to control the timed task. + */ + abstract startTimed( + task: CronTask, + cb: ( + /** + * The date of the next tick. + */ + date: Date, + /** + * A table to control the timed task. + */ + cancel: TimedTab, + ) => void, + ): TimedTab; + + /** + * Returns an async generator that yields the next tick of the task. + * todo: move implementation out of abstract `TimedTaskScheduler` + * + * @param task - The task to schedule. + * @returns An async generator that yields the next tick of the task. + * + * @example + * ```ts + * // Runs a cron task every day at midnight forever. + * const cronTask: CronTask = { + * name: "daily-task", + * cron: "0 0 * * *", + * }; + * for await (const date of scheduler.iterateTimed(cronTask)) { + * console.log(`Today is ${date}`); + * } + */ + iterateTimed(task: CronTask): AsyncIterable { + return { + [Symbol.asyncIterator]: () => { + /** + * There are two callback that consumes each other. + * + * If an iterator callback is first call, `resolveResult` is set. + * Then, the timed callback will call `resolveResult` with the date of the next tick. + * + * If a timed callback is first call, the date is pushed to a linked list with the date. + * Then, the iterator callback will return the date of the next tick. + */ + let resolveResult: ((date: Date) => void) | undefined; + + /** + * A linked list to store the dates of the next ticks. + */ + interface TimedList { + /** + * The date of the next tick. + */ + date: Date; + /** + * The next node in the linked list. + */ + next?: TimedList; + } + let head: TimedList | undefined; + let tail: TimedList | undefined; + + // Starts a timed task to run. + const tab = this.startTimed(task, (date) => { + if (resolveResult) { + resolveResult(date); + resolveResult = undefined; + } else { + const node: TimedList = { date }; + if (head) { + tail!.next = node; + } else { + head = node; + } + tail = node; + } + }); + + return { + next: async () => { + if (tab.cancelled) { + return { value: undefined, done: true }; + } + if (head) { + const node = head; + head = node.next; + if (!head) { + tail = undefined; + } + return { value: node.date, done: false }; + } + return new Promise((resolve) => { + resolveResult = (value) => resolve({ value, done: false }); + }); + }, + return: async () => { + tab.cancel(); + return { value: undefined, done: true }; + }, + }; + }, + }; + } +} diff --git a/packages/ema/src/types.ts b/packages/ema/src/types.ts new file mode 100644 index 00000000..d2ad09ab --- /dev/null +++ b/packages/ema/src/types.ts @@ -0,0 +1,18 @@ +/** + * Type checking utilities. + */ +/** + * Check if A is a subtype of B. + */ +export type Is = A extends B ? true : false; +/** + * Check if A and B are equal. + */ +export type Equal = + (() => T extends A ? 1 : 2) extends () => T extends B ? 1 : 2 + ? true + : false; +/** + * Expect T to be true. + */ +export type Expect = Equal; diff --git a/scripts/docs.js b/scripts/docs.js index 5ee41adc..9887c671 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -5,11 +5,19 @@ import { globSync } from "fs"; * Generate the documentation for the core and HTTP endpoints. */ function docsGen() { + const entryFlag = (it) => `--entryPoints ${it}`; + + const coreEntries = [ + "packages/ema/src/config.ts", + "packages/ema/src/db/index.ts", + "packages/ema/src/concept/index.ts", + "packages/ema/src/skills/index.ts", + ].map(entryFlag); execSync( - "typedoc --entryPoints packages/ema/src/index.ts --entryPoints packages/ema/src/config.ts --entryPoints packages/ema/src/db/index.ts --entryPoints packages/ema/src/skills/index.ts --tsconfig packages/ema/tsconfig.json --out docs/core", + `typedoc ${coreEntries.join(" ")} --tsconfig packages/ema/tsconfig.json --out docs/core`, ); const routes = globSync("packages/ema-ui/src/app/api/**/route.ts").map( - (it) => `--entryPoints ${it}`, + entryFlag, ); execSync( `typedoc ${routes.join(" ")} --tsconfig packages/ema-ui/tsconfig.json --out docs/http`,