From 6aa02bbb77b2b4b38f7bd958311a95a98f2c9ac8 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 30 Dec 2025 19:34:56 +0800 Subject: [PATCH 01/29] docs: draft llm client and actor input/output interfaces. --- docs/.vitepress/config.js | 38 ++++++++++ packages/ema/src/actor.ts | 2 +- packages/ema/src/concept/actor.ts | 103 ++++++++++++++++++++++++++ packages/ema/src/concept/index.ts | 53 ++++++++++++++ packages/ema/src/concept/llm.ts | 20 ++++++ packages/ema/src/concept/storage.ts | 107 ++++++++++++++++++++++++++++ scripts/docs.js | 15 ++-- 7 files changed, 333 insertions(+), 5 deletions(-) create mode 100644 packages/ema/src/concept/actor.ts create mode 100644 packages/ema/src/concept/index.ts create mode 100644 packages/ema/src/concept/llm.ts create mode 100644 packages/ema/src/concept/storage.ts diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 49e4192f..21237965 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 = 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 0c0a30e1..274b1d6f 100644 --- a/packages/ema/src/actor.ts +++ b/packages/ema/src/actor.ts @@ -14,7 +14,7 @@ import type { LongTermMemory, ActorStateStorage, ActorMemory, -} from "./memory/memory"; +} from "./concept"; import { LLMClient } from "./llm"; /** diff --git a/packages/ema/src/concept/actor.ts b/packages/ema/src/concept/actor.ts new file mode 100644 index 00000000..6955ee5f --- /dev/null +++ b/packages/ema/src/concept/actor.ts @@ -0,0 +1,103 @@ +import { EventEmitter } from "node:events"; + +/** + * You can use {@link ActorClient} APIs to communicate with the actor. + * The actors are the core components of the system. They are responsible for taking user inputs and generating outputs. + * Each actor holds resources, such as LLM memory, tools, database storage, etc. + */ +export interface ActorClient { + /** + * The event source of the actor client. + */ + events: EventEmitter & ActorEventSource; + + /** + * Adds a batch of {@link ActorInput} to the actor's input queue. + * @param inputs - The batch of inputs to add to the actor's input queue. + * @returns Promise resolving when the batch of inputs is added to the input queue. + */ + addInputs(inputs: ActorInput[]): Promise; +} + +/** + * The event map for the actor client. + */ +export interface ActorClientEventMap { + /** + * Emitted when the actor has processed the input and generated the output. + */ + output: ActorMessageEvent[]; +} + +/** + * The input to the actor, including text, image, audio, video, etc. + */ +export type ActorInput = ActorTextInput; + +/** + * The text input to the actor. + */ +export interface ActorTextInput { + /** + * The kind of the input. + */ + kind: "text"; + /** + * The content of the input. + */ + content: string; +} + +/** + * A event from the actor. + */ +export type ActorEvent = ActorMessageEvent; + +/** + * A message from the actor. + */ +export interface ActorMessageEvent { + /** + * The kind of the event. + */ + kind: "message"; + /** + * The content of the message. + */ + content: string; +} + +/** Following are extended friendly typings. */ + +/** + * The event source for the actor client. + */ +interface ActorEventSource extends EventEmitter { + /** + * Subscribes to the actor's output ({@link ActorEvent}). + * @param event - The event to subscribe to. + * @param callback - The callback to call when the event is emitted. + * @returns The actor client. + */ + on(event: "output", callback: (event: ActorMessageEvent) => void): this; + /** + * Unsubscribes from the actor's output ({@link ActorEvent}). + * @param event - The event to unsubscribe from. + * @param callback - The callback to unsubscribe from. + * @returns The actor client. + */ + off(event: "output", callback: (event: ActorMessageEvent) => void): this; + /** + * Subscribes to the actor's output ({@link ActorEvent}) once. + * @param event - The event to subscribe to. + * @param callback - The callback to call when the event is emitted. + * @returns The actor client. + */ + once(event: "output", callback: (event: ActorMessageEvent) => void): this; + /** + * Emits events from the actor. + * @param events - The events to emit. + * @returns True if the event was emitted, false otherwise. + */ + emit(event: "output", ...data: ActorMessageEvent[]): boolean; +} diff --git a/packages/ema/src/concept/index.ts b/packages/ema/src/concept/index.ts new file mode 100644 index 00000000..4e90337c --- /dev/null +++ b/packages/ema/src/concept/index.ts @@ -0,0 +1,53 @@ +/** + * This module defines the concept of the EverMemoryArchive. + * + * - UI is the user interface. Users can interacts with ema using WebUI, NapCatQQ, or TUI. + * - Ema Actor is the actor that takes user inputs and generates outputs. + * - Visit an actor instance using {@link ActorClient}. + * - LLM is the LLM that is responsible for the generation of the response. + * - Visit llm providers using {@link EmaLLMClient}. + * - Storage is the storage that is responsible for the storage of the data. + * + * ```mermaid + * graph TD + * %% UI Layer + * subgraph ui_layer ["UI Layer"] + * direction TB + * WebUI[Web UI] + * NapCat[NapCatQQ] + * TUI[Terminal UI] + * end + * + * %% Ema Actor + * Actor[Ema Actor] + * + * %% Storage Layer + * subgraph storage_group ["Storage Layer"] + * direction TB + * MongoDB[MongoDB] + * LanceDB[LanceDB] + * end + * + * %% LLM Layer + * subgraph llm_group ["LLM Layer"] + * direction TB + * OpenAI[OpenAI] + * Google[Google GenAI] + * end + * + * %% Relationships - vertical flow + * ui_layer <--> Actor + * Actor --> storage_group + * Actor --> llm_group + * ``` + * + * @module @internals/concept + */ + +export { type ActorClient } from "./actor"; +export * from "./actor"; + +export { type EmaLLMClient } from "./llm"; +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..5c2ec031 --- /dev/null +++ b/packages/ema/src/concept/llm.ts @@ -0,0 +1,20 @@ +import type { Message, LLMResponse } from "../schema"; + +/** + * {@link LLMClient} is a stateless client for the LLM, holding a physical network connection to the LLM. + * + * TODO: remove mini-agent's LLMClient definition. + */ +export interface EmaLLMClient { + /** + * Generates response from LLM. + * + * @param messages - List of conversation messages + * @param tools - Optional list of Tool objects or dicts + * @returns LLMResponse containing the generated content, thinking, and tool calls + */ + generate(messages: Message[], tools?: Tool[]): Promise; +} + +// TODO: definition of tools. +export type Tool = any; diff --git a/packages/ema/src/concept/storage.ts b/packages/ema/src/concept/storage.ts new file mode 100644 index 00000000..9bf18ea6 --- /dev/null +++ b/packages/ema/src/concept/storage.ts @@ -0,0 +1,107 @@ +import type { Message } from "../schema"; + +/** + * Interface for persisting actor state + */ +export interface ActorStateStorage { + /** + * Gets the state of the actor + * @returns Promise resolving to the state of the actor + */ + getState(): Promise; + /** + * Updates the state of the actor + * @param state - The state to update + * @returns Promise resolving when the state is updated + */ + updateState(state: ActorState): Promise; +} + +export interface ActorState { + /** + * The memory buffer, in the format of messages in OpenAI chat completion API. + */ + + memoryBuffer: Message[]; + // more state can be added here. +} + +/** + * Interface for actor memory + */ +export interface ActorMemory { + /** + * Searches actor memory + * @param keywords - Keywords to search for + * @returns Promise resolving to the search result + */ + search(keywords: string[]): Promise; + /** + * Adds short term memory + * @param item - Short term memory item + * @returns Promise resolving when the memory is added + */ + addShortTermMemory(item: ShortTermMemory): Promise; + /** + * Adds long term memory + * @param item - Long term memory item + * @returns Promise resolving when the memory is added + */ + addLongTermMemory(item: LongTermMemory): Promise; +} + +/** + * Result of searching agent memory + */ +export interface SearchActorMemoryResult { + /** + * The long term memories found + */ + items: LongTermMemory[]; +} + +export interface ShortTermMemory { + /** + * The granularity of short term memory + */ + kind: "year" | "month" | "day"; + /** + * The os when the actor saw the messages. + */ + os: string; + /** + * The statement when the actor saw the messages. + */ + statement: string; + /** + * The date and time the memory was created + */ + createdAt: number; +} + +export interface LongTermMemory { + /** + * The 0-index to search, a.k.a. 一级分类 + */ + index0: string; + /** + * The 1-index to search, a.k.a. 二级分类 + */ + index1: string; + /** + * The keywords to search + */ + keywords: string[]; + /** + * The os when the actor saw the messages. + */ + os: string; + /** + * The statement when the actor saw the messages. + */ + statement: string; + /** + * The date and time the memory was created + */ + createdAt: number; +} diff --git a/scripts/docs.js b/scripts/docs.js index 5ee41adc..06a85fc0 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -5,12 +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/index.ts", + "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", - ); - const routes = globSync("packages/ema-ui/src/app/api/**/route.ts").map( - (it) => `--entryPoints ${it}`, + `typedoc ${coreEntries.join(" ")} --tsconfig packages/ema/tsconfig.json --out docs/core`, ); + const routes = globSync("packages/ema-ui/src/app/api/**/route.ts").map(entryFlag); execSync( `typedoc ${routes.join(" ")} --tsconfig packages/ema-ui/tsconfig.json --out docs/http`, ); From 14d1e2aa42b854d30a3eedaca0680037ce52b536 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 30 Dec 2025 20:02:15 +0800 Subject: [PATCH 02/29] fix: valid id --- docs/.vitepress/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/.vitepress/config.js b/docs/.vitepress/config.js index 21237965..4b5f0c1d 100644 --- a/docs/.vitepress/config.js +++ b/docs/.vitepress/config.js @@ -77,7 +77,7 @@ document.addEventListener('DOMContentLoaded', async () => { const preCode = code.querySelector('pre code'); if (preCode) { const codeText = preCode.textContent; - const randomId = Math.random().toString(36).substring(2, 15); + const randomId = "mermaid-" + Math.random().toString(36).substring(2, 15); const {svg} = await mermaid.render(randomId, codeText); code.innerHTML = svg; } From c86370ce53253196fdbc89a6a332a34a6e7c0c30 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 30 Dec 2025 20:38:57 +0800 Subject: [PATCH 03/29] dev: improve actor docs --- packages/ema/src/concept/actor.ts | 35 ++++++++++++++++++++++++++++--- packages/ema/src/types.ts | 18 ++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) create mode 100644 packages/ema/src/types.ts diff --git a/packages/ema/src/concept/actor.ts b/packages/ema/src/concept/actor.ts index 6955ee5f..2eeb8ff8 100644 --- a/packages/ema/src/concept/actor.ts +++ b/packages/ema/src/concept/actor.ts @@ -4,10 +4,32 @@ import { EventEmitter } from "node:events"; * You can use {@link ActorClient} APIs to communicate with the actor. * The actors are the core components of the system. They are responsible for taking user inputs and generating outputs. * Each actor holds resources, such as LLM memory, tools, database storage, etc. + * + * - Receive inputs from the user by {@link ActorClient.addInputs}. + * - Subscribe to the actor's events by {@link ActorClient.events}. + * - See output events ({@link ActorClientEventMap.output}). + * + * @example + * ```ts + * // Add inputs to the actor. + * const actor: ActorClient; + * actor.addInputs([{ kind: "text", content: "Hello, world!" }]); + * ``` + * + * @example + * ```ts + * // Subscribe to the actor's output events. + * const actor: ActorClient; + * actor.events.on("output", (event) => { + * if (event.kind === "message") { + * console.log(event.content); + * } + * }); + * ``` */ export interface ActorClient { /** - * The event source of the actor client. + * The event source of the actor client. See {@link ActorEventSource} for more details. */ events: EventEmitter & ActorEventSource; @@ -24,7 +46,7 @@ export interface ActorClient { */ export interface ActorClientEventMap { /** - * Emitted when the actor has processed the input and generated the output. + * Emit {@link ActorMessageEvent} when the actor has processed the input and generated the output. */ output: ActorMessageEvent[]; } @@ -72,7 +94,7 @@ export interface ActorMessageEvent { /** * The event source for the actor client. */ -interface ActorEventSource extends EventEmitter { +export interface ActorEventSource { /** * Subscribes to the actor's output ({@link ActorEvent}). * @param event - The event to subscribe to. @@ -101,3 +123,10 @@ interface ActorEventSource extends EventEmitter { */ emit(event: "output", ...data: ActorMessageEvent[]): boolean; } + +import type { Expect, Is } from "../types"; + +type Cases = [ + // EventEmitter must be a subtype of ActorEventSource. + Expect, ActorEventSource>>, +]; 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; From a653bd92f7a6ba9b33f558766312a3709a36dbb5 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 30 Dec 2025 21:02:44 +0800 Subject: [PATCH 04/29] fix: format --- scripts/docs.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scripts/docs.js b/scripts/docs.js index 06a85fc0..28a61e6c 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -5,7 +5,7 @@ import { globSync } from "fs"; * Generate the documentation for the core and HTTP endpoints. */ function docsGen() { - const entryFlag = it => `--entryPoints ${it}`; + const entryFlag = (it) => `--entryPoints ${it}`; const coreEntries = [ "packages/ema/src/index.ts", @@ -13,11 +13,13 @@ function docsGen() { "packages/ema/src/db/index.ts", "packages/ema/src/concept/index.ts", "packages/ema/src/skills/index.ts", - ].map(entryFlag) + ].map(entryFlag); execSync( `typedoc ${coreEntries.join(" ")} --tsconfig packages/ema/tsconfig.json --out docs/core`, ); - const routes = globSync("packages/ema-ui/src/app/api/**/route.ts").map(entryFlag); + const routes = globSync("packages/ema-ui/src/app/api/**/route.ts").map( + entryFlag, + ); execSync( `typedoc ${routes.join(" ")} --tsconfig packages/ema-ui/tsconfig.json --out docs/http`, ); From 27e71167886e8a298e5e182e2a214002a8a13037 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Tue, 30 Dec 2025 21:11:01 +0800 Subject: [PATCH 05/29] Update packages/ema/src/concept/actor.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/ema/src/concept/actor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ema/src/concept/actor.ts b/packages/ema/src/concept/actor.ts index 2eeb8ff8..459785fb 100644 --- a/packages/ema/src/concept/actor.ts +++ b/packages/ema/src/concept/actor.ts @@ -121,7 +121,7 @@ export interface ActorEventSource { * @param events - The events to emit. * @returns True if the event was emitted, false otherwise. */ - emit(event: "output", ...data: ActorMessageEvent[]): boolean; + emit(event: "output", ...events: ActorMessageEvent[]): boolean; } import type { Expect, Is } from "../types"; From d79e670871c07bdafbd281feb218b5b6ddfaae3d Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Tue, 30 Dec 2025 21:11:08 +0800 Subject: [PATCH 06/29] Update packages/ema/src/concept/llm.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/ema/src/concept/llm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 5c2ec031..265bdc6f 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -1,7 +1,7 @@ import type { Message, LLMResponse } from "../schema"; /** - * {@link LLMClient} is a stateless client for the LLM, holding a physical network connection to the LLM. + * {@link EmaLLMClient} is a stateless client for the LLM, holding a physical network connection to the LLM. * * TODO: remove mini-agent's LLMClient definition. */ From 958ea4ca189505addfc7df8bd6e550db8bb9880f Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Sat, 3 Jan 2026 20:24:09 +0800 Subject: [PATCH 07/29] draft --- packages/ema/src/concept/llm.ts | 50 +++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 265bdc6f..fb19d034 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -18,3 +18,53 @@ export interface EmaLLMClient { // TODO: definition of tools. export type Tool = any; + +/** + * {@link Agent} is a background-running thread that communicates with the actor. + */ +export interface Agent { + S: typeof AgentScheReqFactory; + + /** + * Runs the agent. + * + * @returns void + */ + run(scheReq: AgentScheRequest): Promise; +} + +interface AgentState { + memoryBuffer: Message[]; +} + +type AgentScheRequest = AgentScheMessageRequest | AgentScheStateCallbackRequest; + +interface AgentScheMessageRequest { + kind: "msg"; + message: Message; +} + +type AgentScheStateCallback = (state: AgentState) => void; + +interface AgentScheStateCallbackRequest { + kind: "stateCb"; + stateCallback: AgentScheStateCallback; +} + +export class AgentScheReqFactory { + static withMessage(message: Message): AgentScheMessageRequest { + return { + kind: "msg", + message, + }; + } + + static withStateCallback( + stateCallback: AgentScheStateCallback, + ): AgentScheStateCallbackRequest { + return { + kind: "stateCb", + stateCallback, + }; + } +} From 1c57e86ca73be159129eb958bfa930e156d4b158 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 12:07:23 +0800 Subject: [PATCH 08/29] feat: finish agent interface --- packages/ema/src/concept/llm.ts | 112 +++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index fb19d034..a9dda5b2 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -20,51 +20,85 @@ export interface EmaLLMClient { export type Tool = any; /** - * {@link Agent} is a background-running thread that communicates with the actor. + * The state of the agent. + * More state could be added for specific agents, e.g. `memoryBuffer` for agents who have long-term memory. */ -export interface Agent { - S: typeof AgentScheReqFactory; - +interface AgentState { /** - * Runs the agent. - * - * @returns void + * The history of the agent. */ - run(scheReq: AgentScheRequest): Promise; -} - -interface AgentState { - memoryBuffer: Message[]; -} - -type AgentScheRequest = AgentScheMessageRequest | AgentScheStateCallbackRequest; - -interface AgentScheMessageRequest { - kind: "msg"; - message: Message; + history: Message[]; + /** + * The tools of the agent. + */ + tools: Tool[]; } -type AgentScheStateCallback = (state: AgentState) => void; - -interface AgentScheStateCallbackRequest { - kind: "stateCb"; - stateCallback: AgentScheStateCallback; -} +/** + * The state callback of the agent. You can visit the state in the callback, + * and call the `next` function to continue to run the next callback. + * + * - The next function can only be called once. + * - If the next is not called, the agent will keep state change but will not run. + * + * @param state - The state of the agent. + * @param next - The next function to call. + * @returns The state of the agent. + * + * @example + * ```ts + * // Run with additional messages. + * const agent = new Agent(); + * agent.run((state, next) => { + * state.history.push(new Message("user", "Hello, world!")); + * next(); + * return state; + * }); + * ``` + * + * @example + * ```ts + * // Run without saving history + * const agent = new Agent(); + * agent.run((state, next) => { + * const messages = state.history; + * state.history.push(new Message("user", "Hello, world!")); + * next(); + * state.history = messages; + * return state; + * }); + * ``` + */ +export type AgentStateCallback = ( + state: S, + next: () => void, +) => S; -export class AgentScheReqFactory { - static withMessage(message: Message): AgentScheMessageRequest { - return { - kind: "msg", - message, - }; - } +/** + * {@link Agent} is a background-running thread that communicates with the actor. + */ +export abstract class Agent { + /** + * Runs the agent with a state callback. + * + * See {@link AgentStateCallback} for examples. + * + * @param stateCallback - The state callback to run the agent with. + * @returns void + */ + abstract run(stateCallback: AgentStateCallback): Promise; - static withStateCallback( - stateCallback: AgentScheStateCallback, - ): AgentScheStateCallbackRequest { - return { - kind: "stateCb", - stateCallback, - }; + /** + * Runs the agent with a user message. + * + * @param message - The message to run the agent with. + * @returns void + */ + runWithMessage(message: Message): Promise { + return this.run((s, next) => { + s.history.push(message); + next(); + return s; + }); } } From 703289875acbb683ee1ecb2af086aea92af3d1d7 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 12:40:28 +0800 Subject: [PATCH 09/29] dev: design scheduler --- packages/ema/src/concept/llm.ts | 92 ++++++++++++++++++++++++++++----- 1 file changed, 80 insertions(+), 12 deletions(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index a9dda5b2..a79d469f 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -47,23 +47,23 @@ interface AgentState { * * @example * ```ts - * // Run with additional messages. - * const agent = new Agent(); - * agent.run((state, next) => { + * // Runs with additional messages. + * const agent = new AgentImpl(); + * agent.run(async (state, next) => { * state.history.push(new Message("user", "Hello, world!")); - * next(); + * await next(); * return state; * }); * ``` * * @example * ```ts - * // Run without saving history - * const agent = new Agent(); - * agent.run((state, next) => { + * // Runs without saving history + * const agent = new AgentImpl(); + * agent.run(async (state, next) => { * const messages = state.history; * state.history.push(new Message("user", "Hello, world!")); - * next(); + * await next(); * state.history = messages; * return state; * }); @@ -71,8 +71,8 @@ interface AgentState { */ export type AgentStateCallback = ( state: S, - next: () => void, -) => S; + next: () => Promise, +) => Promise; /** * {@link Agent} is a background-running thread that communicates with the actor. @@ -95,10 +95,78 @@ export abstract class Agent { * @returns void */ runWithMessage(message: Message): Promise { - return this.run((s, next) => { + return this.run(async (s, next) => { s.history.push(message); - next(); + await next(); return s; }); } } + +interface AgentTask { + /** + * A human-readable name of the task. + */ + name: string; + /** + * A cron expression of the task. + * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. + * - Use {@link https://crontab.guru/} to create cron expressions. + * + * If this is not provided, the task will run once. + */ + cron?: string; + + /** + * Runs the task with the agent and scheduler. + * + * @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. + * + * @example + * ```ts + * // Runs the task every day at midnight forever. + * scheduler.schedule({ + * name: "daily-task", + * cron: "0 0 * * *", + * async run(agent, scheduler) { + * await agent.runWithMessage(new Message("user", "Hello, world!")); + * }, + * }); + * ``` + * + * @example + * ```ts + * // Cancels the task. + * scheduler.schedule({ + * name: "daily-task", + * cron: "0 0 * * *", + * async run(agent, scheduler) { + * await scheduler.cancel(this); + * }, + * }); + * ``` + */ + run(agent: Agent, scheduler: AgentScheduler): Promise; +} + +/** + * The scheduler of the agent. A scheduler manages multiple llm sessions with a sensible resource limits. + */ +export interface AgentScheduler { + /** + * Schedules a task to run. + * + * @param task - The task to schedule. + * @returns Promise resolving when the task is scheduled. + */ + schedule(task: AgentTask): Promise; + /** + * Cancels a task to run. + * + * @param task - The task to cancel. + * @returns Promise resolving when the task is canceled. + */ + cancel(task: AgentTask): Promise; +} From 87fe8b279180a91cbcfc0be6b15e66cf806db99a Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 12:40:54 +0800 Subject: [PATCH 10/29] dev: space --- packages/ema/src/concept/llm.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index a79d469f..ede3f8b7 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -108,6 +108,7 @@ interface AgentTask { * A human-readable name of the task. */ name: string; + /** * A cron expression of the task. * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. From ad0d3faa06bafe6570751e54d263b341297f27e6 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 12:49:45 +0800 Subject: [PATCH 11/29] dev: agent arg --- packages/ema/src/concept/llm.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index ede3f8b7..653556a0 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -118,6 +118,12 @@ interface AgentTask { */ cron?: string; + /** + * 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 scheduler. * @@ -144,7 +150,11 @@ interface AgentTask { * name: "daily-task", * cron: "0 0 * * *", * async run(agent, scheduler) { - * await scheduler.cancel(this); + * if (timeIsAfter('2026-01-01')) { + * await scheduler.cancel(this); + * return; + * } + * await agent.runWithMessage(new Message("user", "Hello, world!")); * }, * }); * ``` From 48cfe16d766663b616bd08d8dc306ea82d70a490 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 15:28:06 +0800 Subject: [PATCH 12/29] dev: split cron function --- packages/ema/src/concept/index.ts | 4 +- packages/ema/src/concept/llm.ts | 90 ++++++++++++++++++++----------- 2 files changed, 63 insertions(+), 31 deletions(-) diff --git a/packages/ema/src/concept/index.ts b/packages/ema/src/concept/index.ts index 4e90337c..4886824b 100644 --- a/packages/ema/src/concept/index.ts +++ b/packages/ema/src/concept/index.ts @@ -6,6 +6,8 @@ * - Visit an actor instance using {@link ActorClient}. * - LLM is the LLM that is responsible for the generation of the response. * - Visit llm providers using {@link EmaLLMClient}. + * - Create a stateful agent by extending {@link Agent}. + * - Run a task with {@link AgentScheduler} by providing {@link AgentTask}. * - Storage is the storage that is responsible for the storage of the data. * * ```mermaid @@ -47,7 +49,7 @@ export { type ActorClient } from "./actor"; export * from "./actor"; -export { type EmaLLMClient } from "./llm"; +export type { EmaLLMClient, Agent, AgentTask, AgentScheduler } from "./llm"; export * from "./llm"; export * from "./storage"; diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 653556a0..f43ec100 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -103,21 +103,12 @@ export abstract class Agent { } } -interface AgentTask { +export interface AgentTask { /** * A human-readable name of the task. */ name: string; - /** - * A cron expression of the task. - * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. - * - Use {@link https://crontab.guru/} to create cron expressions. - * - * If this is not provided, the task will run once. - */ - cron?: string; - /** * The agent to run the task with. * If not provided, the task will run with a new agent. @@ -125,7 +116,7 @@ interface AgentTask { agent?: Agent; /** - * Runs the task with the agent and scheduler. + * 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. @@ -134,27 +125,17 @@ interface AgentTask { * @example * ```ts * // Runs the task every day at midnight forever. - * scheduler.schedule({ + * const cronTab: CronTab = { * name: "daily-task", * cron: "0 0 * * *", - * async run(agent, scheduler) { - * await agent.runWithMessage(new Message("user", "Hello, world!")); - * }, - * }); - * ``` - * - * @example - * ```ts - * // Cancels the task. + * }; * scheduler.schedule({ * name: "daily-task", - * cron: "0 0 * * *", * async run(agent, scheduler) { - * if (timeIsAfter('2026-01-01')) { - * await scheduler.cancel(this); - * return; + * while(nextTick(cronTab)) { + * await scheduler.waitForIdle(agent); + * await agent.runWithMessage(new Message("user", "Hello, world!")); * } - * await agent.runWithMessage(new Message("user", "Hello, world!")); * }, * }); * ``` @@ -166,6 +147,15 @@ interface AgentTask { * The scheduler of the agent. A scheduler manages multiple llm sessions with a sensible resource limits. */ export interface AgentScheduler { + /** + * Runs an oneshot task. + * + * @param cb - The callback to run the task. + * @returns Promise resolving when the task is completed. + */ + run( + cb: (agent: Agent) => Promise, + ): Promise; /** * Schedules a task to run. * @@ -174,10 +164,50 @@ export interface AgentScheduler { */ schedule(task: AgentTask): Promise; /** - * Cancels a task to run. + * Waits for the agent to be idle. * - * @param task - The task to cancel. - * @returns Promise resolving when the task is canceled. + * @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. */ - cancel(task: AgentTask): Promise; + waitForIdle(agent: Agent, timeout?: number): Promise; + /** + * Checks if the agent is running. + * + * @param agent - The agent to check. + * @returns Whether the agent is running. + */ + isRunning(agent: Agent): boolean; + /** + * Stops the agent unconditionally. + * + * @param agent - The agent to stop. + */ + stop(agent: Agent): Promise; +} + +/** + * A cron tab is a descriptor of a cron job. + * + * @example + * ```ts + * const cronTab: CronTab = { + * name: "daily-task", + * cron: "0 0 * * *", + * }; + * ``` + */ +export interface CronTab { + /** + * A human-readable name of the cron tab. + */ + name: string; + /** + * A cron expression of the task. + * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. + * - Use {@link https://crontab.guru/} to create cron expressions. + * + * If this is not provided, the task will run once. + */ + cron?: string; } From 2fff0e6720aa799181a87704956a72df179e67bf Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 15:36:28 +0800 Subject: [PATCH 13/29] dev: agent events --- packages/ema/src/concept/llm.ts | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index f43ec100..56dceb34 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -1,3 +1,4 @@ +import type { EventEmitter } from "node:events"; import type { Message, LLMResponse } from "../schema"; /** @@ -78,6 +79,11 @@ export type AgentStateCallback = ( * {@link Agent} is a background-running thread that communicates with the actor. */ export abstract class Agent { + /** + * The event source of the agent. See {@link AgentEventSource} for more details. + */ + abstract events: EventEmitter & AgentEventSource; + /** * Runs the agent with a state callback. * @@ -103,6 +109,15 @@ export abstract class Agent { } } +/** + * The event map for the agent. + */ +export interface AgentEventMap { + // todo: agent events. maybe: + // 1. agent output + // 2. special agent event, for example, we can define "memory:reorgnize" to tell other components the agent is reorgnizing memory. +} + export interface AgentTask { /** * A human-readable name of the task. @@ -211,3 +226,10 @@ export interface CronTab { */ cron?: string; } + +/** Following are extended friendly typings. */ + +/** + * The event source for the agent. + */ +export interface AgentEventSource {} From 6d216a4025be5f3c946e04cbad2df173958e2593 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 15:38:14 +0800 Subject: [PATCH 14/29] fix: examples --- packages/ema/src/concept/llm.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 56dceb34..483aa676 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -51,7 +51,7 @@ interface AgentState { * // Runs with additional messages. * const agent = new AgentImpl(); * agent.run(async (state, next) => { - * state.history.push(new Message("user", "Hello, world!")); + * state.history.push({ type: "user", content: "Hello, World!" }); * await next(); * return state; * }); @@ -63,7 +63,7 @@ interface AgentState { * const agent = new AgentImpl(); * agent.run(async (state, next) => { * const messages = state.history; - * state.history.push(new Message("user", "Hello, world!")); + * state.history.push({ type: "user", content: "Hello, World!" }); * await next(); * state.history = messages; * return state; @@ -149,7 +149,7 @@ export interface AgentTask { * async run(agent, scheduler) { * while(nextTick(cronTab)) { * await scheduler.waitForIdle(agent); - * await agent.runWithMessage(new Message("user", "Hello, world!")); + * await agent.runWithMessage({ type: "user", content: "Hello, World!" }); * } * }, * }); From 0ce8eeed30205432b174110b40d86fd50af73bfa Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 15:39:18 +0800 Subject: [PATCH 15/29] fix: simplify examples --- packages/ema/src/concept/llm.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 483aa676..daff96d4 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -148,7 +148,6 @@ export interface AgentTask { * name: "daily-task", * async run(agent, scheduler) { * while(nextTick(cronTab)) { - * await scheduler.waitForIdle(agent); * await agent.runWithMessage({ type: "user", content: "Hello, World!" }); * } * }, From 3683b9650b99f87e1b96f80c5c2f38e20640969f Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 15:45:22 +0800 Subject: [PATCH 16/29] dev: move accessor --- packages/ema/src/concept/index.ts | 27 +++++++++++++++ packages/ema/src/concept/llm.ts | 55 +++++++++---------------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/packages/ema/src/concept/index.ts b/packages/ema/src/concept/index.ts index 4886824b..818b5f97 100644 --- a/packages/ema/src/concept/index.ts +++ b/packages/ema/src/concept/index.ts @@ -53,3 +53,30 @@ export type { EmaLLMClient, Agent, AgentTask, AgentScheduler } from "./llm"; export * from "./llm"; export * from "./storage"; + +// todo: move me to a separate file. +/** + * A cron tab is a descriptor of a cron job. + * + * @example + * ```ts + * const cronTab: CronTab = { + * name: "daily-task", + * cron: "0 0 * * *", + * }; + * ``` + */ +export interface CronTab { + /** + * A human-readable name of the cron tab. + */ + name: string; + /** + * A cron expression of the task. + * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. + * - Use {@link https://crontab.guru/} to create cron expressions. + * + * If this is not provided, the task will run once. + */ + cron?: string; +} diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index daff96d4..5bda859f 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -85,7 +85,21 @@ export abstract class Agent { abstract events: EventEmitter & AgentEventSource; /** - * Runs the agent with a state callback. + * Checks if the agent is running a LLM session. + * + * @param agent - The agent to check. + * @returns Whether the agent is running. + */ + abstract isRunning(): boolean; + /** + * Stops the running session unconditionally. + * + * @param agent - The agent to stop. + */ + abstract stop(): Promise; + + /** + * Runs the agent with a state callback. The agent will ensure that it is idle when the callback is called. * * See {@link AgentStateCallback} for examples. * @@ -185,45 +199,6 @@ export interface AgentScheduler { * @returns Promise resolving when the agent is idle or the timeout is reached. */ waitForIdle(agent: Agent, timeout?: number): Promise; - /** - * Checks if the agent is running. - * - * @param agent - The agent to check. - * @returns Whether the agent is running. - */ - isRunning(agent: Agent): boolean; - /** - * Stops the agent unconditionally. - * - * @param agent - The agent to stop. - */ - stop(agent: Agent): Promise; -} - -/** - * A cron tab is a descriptor of a cron job. - * - * @example - * ```ts - * const cronTab: CronTab = { - * name: "daily-task", - * cron: "0 0 * * *", - * }; - * ``` - */ -export interface CronTab { - /** - * A human-readable name of the cron tab. - */ - name: string; - /** - * A cron expression of the task. - * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. - * - Use {@link https://crontab.guru/} to create cron expressions. - * - * If this is not provided, the task will run once. - */ - cron?: string; } /** Following are extended friendly typings. */ From 19c5682c49d0d675a790ec0965a204d80e0e6533 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 15:50:45 +0800 Subject: [PATCH 17/29] fix: comments --- packages/ema/src/concept/llm.ts | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 5bda859f..1eebb0dd 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -87,31 +87,19 @@ export abstract class Agent { /** * Checks if the agent is running a LLM session. * - * @param agent - The agent to check. * @returns Whether the agent is running. */ abstract isRunning(): boolean; /** * Stops the running session unconditionally. * - * @param agent - The agent to stop. */ abstract stop(): Promise; - /** - * Runs the agent with a state callback. The agent will ensure that it is idle when the callback is called. - * - * See {@link AgentStateCallback} for examples. - * - * @param stateCallback - The state callback to run the agent with. - * @returns void - */ - abstract run(stateCallback: AgentStateCallback): Promise; - /** * Runs the agent with a user message. * - * @param message - The message to run the agent with. + * @param message - The message to add. * @returns void */ runWithMessage(message: Message): Promise { @@ -121,6 +109,16 @@ export abstract class Agent { return s; }); } + + /** + * Runs the agent with a state callback. The agent will ensure that it is idle when the callback is called. + * + * See {@link AgentStateCallback} for examples. + * + * @param stateCallback - The state callback to run the agent with. + * @returns Promise resolving when the agent is idle. + */ + abstract run(stateCallback: AgentStateCallback): Promise; } /** From 7293b796e0825721cb3c74ac010fc80cd9479a96 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin <35292584+Myriad-Dreamin@users.noreply.github.com> Date: Tue, 6 Jan 2026 19:07:03 +0800 Subject: [PATCH 18/29] Update packages/ema/src/concept/llm.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/ema/src/concept/llm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 1eebb0dd..f2a593a7 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -174,7 +174,7 @@ export interface AgentTask { */ export interface AgentScheduler { /** - * Runs an oneshot task. + * Runs a one-shot task. * * @param cb - The callback to run the task. * @returns Promise resolving when the task is completed. From c87e3adc67a8d231fd53598bcda8e38e02da6253 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 19:09:44 +0800 Subject: [PATCH 19/29] dev: export agent state --- packages/ema/src/concept/llm.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index f2a593a7..b1da1c4b 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -24,7 +24,7 @@ export type Tool = any; * The state of the agent. * More state could be added for specific agents, e.g. `memoryBuffer` for agents who have long-term memory. */ -interface AgentState { +export interface AgentState { /** * The history of the agent. */ From 6d41d282a72346b14f4a7c2ca5308e68ee0fddbd Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 19:12:05 +0800 Subject: [PATCH 20/29] dev: update description --- packages/ema/src/concept/llm.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index b1da1c4b..c7058ae0 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -97,10 +97,10 @@ export abstract class Agent { abstract stop(): Promise; /** - * Runs the agent with a user message. + * Runs the agent with a message in OpenAI format. * * @param message - The message to add. - * @returns void + * @returns Promise resolving when the agent is idle. */ runWithMessage(message: Message): Promise { return this.run(async (s, next) => { @@ -111,7 +111,8 @@ export abstract class Agent { } /** - * Runs the agent with a state callback. The agent will ensure that it is idle when the callback is called. + * Runs a state callback when the agent becomes idle. If the agent is running a LLM session, the callback will be + * called after the session is finished. * * See {@link AgentStateCallback} for examples. * From 2c4f3c735aabb1fb477fb6533704b81b891b939c Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 6 Jan 2026 19:12:33 +0800 Subject: [PATCH 21/29] dev: update description --- packages/ema/src/concept/llm.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index c7058ae0..9e17dc52 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -128,7 +128,8 @@ export abstract class Agent { export interface AgentEventMap { // todo: agent events. maybe: // 1. agent output - // 2. special agent event, for example, we can define "memory:reorgnize" to tell other components the agent is reorgnizing memory. + // 2. special agent event, for example, we can define "memory:reorganize" to tell other components the agent is + // reorganizing memory. } export interface AgentTask { From f7b9c6c6efc4e6647cd23620d25ed784759581b1 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 13 Jan 2026 18:42:41 +0800 Subject: [PATCH 22/29] dev: add comments --- packages/ema/src/concept/index.ts | 39 ++--- packages/ema/src/concept/llm.ts | 69 --------- packages/ema/src/concept/task.ts | 249 ++++++++++++++++++++++++++++++ 3 files changed, 260 insertions(+), 97 deletions(-) create mode 100644 packages/ema/src/concept/task.ts diff --git a/packages/ema/src/concept/index.ts b/packages/ema/src/concept/index.ts index 818b5f97..0e2d68e5 100644 --- a/packages/ema/src/concept/index.ts +++ b/packages/ema/src/concept/index.ts @@ -7,7 +7,9 @@ * - LLM is the LLM that is responsible for the generation of the response. * - Visit llm providers using {@link EmaLLMClient}. * - Create a stateful agent by extending {@link Agent}. - * - Run a task with {@link AgentScheduler} by providing {@link AgentTask}. + * - Task is the unit of work that can be scheduled and run. A task can implements one or multiple interfaces: + * - Run an agent task with {@link AgentTaskScheduler} by providing {@link AgentTask}. + * - Run a cron task with {@link TimedTaskScheduler} by providing {@link TimedTask}. * - Storage is the storage that is responsible for the storage of the data. * * ```mermaid @@ -49,34 +51,15 @@ export { type ActorClient } from "./actor"; export * from "./actor"; -export type { EmaLLMClient, Agent, AgentTask, AgentScheduler } from "./llm"; +export type { EmaLLMClient, Agent } from "./llm"; export * from "./llm"; export * from "./storage"; -// todo: move me to a separate file. -/** - * A cron tab is a descriptor of a cron job. - * - * @example - * ```ts - * const cronTab: CronTab = { - * name: "daily-task", - * cron: "0 0 * * *", - * }; - * ``` - */ -export interface CronTab { - /** - * A human-readable name of the cron tab. - */ - name: string; - /** - * A cron expression of the task. - * - See {@link https://en.wikipedia.org/wiki/Cron} for more details. - * - Use {@link https://crontab.guru/} to create cron expressions. - * - * If this is not provided, the task will run once. - */ - cron?: string; -} +export type { + AgentTask, + AgentTaskScheduler, + TimedTask, + TimedTaskScheduler, +} from "./task"; +export * from "./task"; diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 9e17dc52..50fe58d7 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -132,75 +132,6 @@ export interface AgentEventMap { // reorganizing memory. } -export interface AgentTask { - /** - * A human-readable name of the task. - */ - name: string; - - /** - * 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. - * - * @example - * ```ts - * // Runs the task every day at midnight forever. - * const cronTab: CronTab = { - * name: "daily-task", - * cron: "0 0 * * *", - * }; - * scheduler.schedule({ - * name: "daily-task", - * async run(agent, scheduler) { - * while(nextTick(cronTab)) { - * await agent.runWithMessage({ type: "user", content: "Hello, World!" }); - * } - * }, - * }); - * ``` - */ - run(agent: Agent, scheduler: AgentScheduler): Promise; -} - -/** - * The scheduler of the agent. A scheduler manages multiple llm sessions with a sensible resource limits. - */ -export interface AgentScheduler { - /** - * Runs a one-shot task. - * - * @param cb - The callback to run the task. - * @returns Promise resolving when the task is completed. - */ - run( - cb: (agent: Agent) => Promise, - ): Promise; - /** - * 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; -} - /** Following are extended friendly typings. */ /** diff --git a/packages/ema/src/concept/task.ts b/packages/ema/src/concept/task.ts new file mode 100644 index 00000000..0371792e --- /dev/null +++ b/packages/ema/src/concept/task.ts @@ -0,0 +1,249 @@ +import type { Agent, AgentState } 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. + * + * @example + * ```ts + * // Runs an agent task every day at midnight forever. + * const dailyTask: CronTask & AgentTask = { + * name: "daily-task", + * cron: "0 0 * * *", + * async work(agent) { + * while(nextTick(this)) { + * await agent.runWithMessage({ type: "user", content: "Hello, World!" }); + * } + * }, + * }; + * 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 _ of scheduler.iterateTimed(cronTask)) { + * console.log(`Today is ${new Date().toISOString()}`); + * } + */ + 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 }; + }, + }; + }, + }; + } +} From 6acadc6f8725541b24502f2d4d08459f2f6e9606 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 13 Jan 2026 18:46:07 +0800 Subject: [PATCH 23/29] dev: update example --- packages/ema/src/concept/task.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/ema/src/concept/task.ts b/packages/ema/src/concept/task.ts index 0371792e..62315e33 100644 --- a/packages/ema/src/concept/task.ts +++ b/packages/ema/src/concept/task.ts @@ -18,6 +18,7 @@ export interface Task { /** * An agent task is a task that runs with an agent. + * - For a timed agent task, use {@link TimedTaskScheduler.iterateTimed}. * * @example * ```ts @@ -26,8 +27,8 @@ export interface Task { * name: "daily-task", * cron: "0 0 * * *", * async work(agent) { - * while(nextTick(this)) { - * await agent.runWithMessage({ type: "user", content: "Hello, World!" }); + * for await (const date of scheduler.iterateTimed(this)) { + * await agent.runWithMessage({ type: "user", content: `Today is ${date}`}); * } * }, * }; @@ -171,8 +172,8 @@ export abstract class TimedTaskScheduler { * name: "daily-task", * cron: "0 0 * * *", * }; - * for await (const _ of scheduler.iterateTimed(cronTask)) { - * console.log(`Today is ${new Date().toISOString()}`); + * for await (const date of scheduler.iterateTimed(cronTask)) { + * console.log(`Today is ${date}`); * } */ iterateTimed(task: CronTask): AsyncIterable { From 9c058680ceb230b6613307f7cc3b3441c9bf4b7c Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 13 Jan 2026 19:19:22 +0800 Subject: [PATCH 24/29] dev: update example --- packages/ema/src/concept/index.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/ema/src/concept/index.ts b/packages/ema/src/concept/index.ts index 0e2d68e5..c02b74dc 100644 --- a/packages/ema/src/concept/index.ts +++ b/packages/ema/src/concept/index.ts @@ -3,9 +3,11 @@ * * - UI is the user interface. Users can interacts with ema using WebUI, NapCatQQ, or TUI. * - Ema Actor is the actor that takes user inputs and generates outputs. - * - Visit an actor instance using {@link ActorClient}. + * - Visit an actor instance using {@link ActorClient}, implemented by `ActorHttpClient` and `ActorWorker`. + * - In frontend, an `ActorHttpClient` connects to some actor in remote server. + * - In frontend or backend, an `ActorWorker` uses LLM to implement the actor logic. * - LLM is the LLM that is responsible for the generation of the response. - * - Visit llm providers using {@link EmaLLMClient}. + * - Visit LLM providers using {@link EmaLLMClient}. * - Create a stateful agent by extending {@link Agent}. * - Task is the unit of work that can be scheduled and run. A task can implements one or multiple interfaces: * - Run an agent task with {@link AgentTaskScheduler} by providing {@link AgentTask}. From da199758247f4650012fe1204fac2814a6d31cd6 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 13 Jan 2026 20:14:47 +0800 Subject: [PATCH 25/29] dev: add stateless api --- packages/ema/src/concept/llm.ts | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 50fe58d7..13034e23 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -25,6 +25,10 @@ export type Tool = any; * More state could be added for specific agents, e.g. `memoryBuffer` for agents who have long-term memory. */ export interface AgentState { + /** + * The system prompt of the agent. + */ + systemPrompt: string; /** * The history of the agent. */ @@ -97,7 +101,21 @@ export abstract class Agent { abstract stop(): Promise; /** - * Runs the agent with a message in OpenAI format. + * Runs the agent in stateless manner. + * + * @param state - The state to run the agent with. + * @returns Promise resolving when the agent is idle. + */ + execute(state: S): Promise { + return this.run(async (s, next) => { + s = state; + await next(); + return s; + }); + } + + /** + * Runs the agent with an additional message in OpenAI format. * * @param message - The message to add. * @returns Promise resolving when the agent is idle. From a2ae0254c151622aa4b058b72fbd15c3d18025f3 Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 13 Jan 2026 20:51:11 +0800 Subject: [PATCH 26/29] dev: update interface --- packages/ema/src/concept/llm.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 13034e23..384f815b 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -30,15 +30,23 @@ export interface AgentState { */ systemPrompt: string; /** - * The history of the agent. + * The messages of the agent to start with. */ - history: Message[]; + messages: Message[]; /** * The tools of the agent. */ tools: Tool[]; } +export interface MutatableAgentState extends AgentState { + /** + * Replaces the state with a new state. + * @param s - The state to replace with. + */ + replace(s: AgentState): void; +} + /** * The state callback of the agent. You can visit the state in the callback, * and call the `next` function to continue to run the next callback. @@ -75,7 +83,7 @@ export interface AgentState { * ``` */ export type AgentStateCallback = ( - state: S, + state: S & MutatableAgentState, next: () => Promise, ) => Promise; @@ -108,7 +116,7 @@ export abstract class Agent { */ execute(state: S): Promise { return this.run(async (s, next) => { - s = state; + s.replace(state); await next(); return s; }); @@ -122,7 +130,7 @@ export abstract class Agent { */ runWithMessage(message: Message): Promise { return this.run(async (s, next) => { - s.history.push(message); + s.messages.push(message); await next(); return s; }); From 99136c3749ed1503cc076523c9774ea63db5e82b Mon Sep 17 00:00:00 2001 From: Myriad-Dreamin Date: Tue, 13 Jan 2026 20:52:10 +0800 Subject: [PATCH 27/29] dev: update interface --- packages/ema/src/concept/llm.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index 384f815b..ba90d4ad 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -12,9 +12,14 @@ export interface EmaLLMClient { * * @param messages - List of conversation messages * @param tools - Optional list of Tool objects or dicts + * @param systemPrompt - Optional system prompt to start with * @returns LLMResponse containing the generated content, thinking, and tool calls */ - generate(messages: Message[], tools?: Tool[]): Promise; + generate( + messages: Message[], + tools?: Tool[], + systemPrompt?: string, + ): Promise; } // TODO: definition of tools. From e7cad00a6de8e93e9f1c7ddef95b2a26b1c99404 Mon Sep 17 00:00:00 2001 From: Disviel Date: Sun, 1 Mar 2026 22:28:56 +0800 Subject: [PATCH 28/29] feat: synchronized documents --- packages/ema/src/actor.ts | 14 +- packages/ema/src/concept/actor.ts | 196 ++++++++-------- packages/ema/src/concept/compat.ts | 41 ++++ packages/ema/src/concept/index.ts | 85 +++---- packages/ema/src/concept/llm.ts | 344 ++++++++++++++++++---------- packages/ema/src/concept/storage.ts | 173 ++++++++++---- scripts/docs.js | 1 - 7 files changed, 542 insertions(+), 312 deletions(-) create mode 100644 packages/ema/src/concept/compat.ts diff --git a/packages/ema/src/actor.ts b/packages/ema/src/actor.ts index 274b1d6f..aca0e7f3 100644 --- a/packages/ema/src/actor.ts +++ b/packages/ema/src/actor.ts @@ -14,11 +14,16 @@ import type { LongTermMemory, ActorStateStorage, ActorMemory, -} from "./concept"; +} from "./concept/compat"; import { LLMClient } from "./llm"; /** - * 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 implements ActorStateStorage, ActorMemory { /** The agent instance. */ @@ -56,7 +61,10 @@ export class ActorWorker implements ActorStateStorage, ActorMemory { /** * A low-level function to step the actor. - * Currently, we ensure that the actor processes the input sequentially. + * + * In current production runtime, inputs are batched and queued, and a busy run + * may be interrupted by newer user input. This compatibility implementation + * keeps a simpler sequential flow. * * @param input - The input to the actor. * @example diff --git a/packages/ema/src/concept/actor.ts b/packages/ema/src/concept/actor.ts index 459785fb..8f48c330 100644 --- a/packages/ema/src/concept/actor.ts +++ b/packages/ema/src/concept/actor.ts @@ -1,132 +1,132 @@ -import { EventEmitter } from "node:events"; +import type { EventEmitter } from "node:events"; +import type { Content as InputContent } from "../schema"; +import type { AgentEventName, AgentEvent, AgentEventUnion } from "./llm"; /** - * You can use {@link ActorClient} APIs to communicate with the actor. - * The actors are the core components of the system. They are responsible for taking user inputs and generating outputs. - * Each actor holds resources, such as LLM memory, tools, database storage, etc. - * - * - Receive inputs from the user by {@link ActorClient.addInputs}. - * - Subscribe to the actor's events by {@link ActorClient.events}. - * - See output events ({@link ActorClientEventMap.output}). - * - * @example - * ```ts - * // Add inputs to the actor. - * const actor: ActorClient; - * actor.addInputs([{ kind: "text", content: "Hello, world!" }]); - * ``` - * - * @example - * ```ts - * // Subscribe to the actor's output events. - * const actor: ActorClient; - * actor.events.on("output", (event) => { - * if (event.kind === "message") { - * console.log(event.content); - * } - * }); - * ``` + * The scope information for the actor. */ -export interface ActorClient { - /** - * The event source of the actor client. See {@link ActorEventSource} for more details. - */ - events: EventEmitter & ActorEventSource; - - /** - * Adds a batch of {@link ActorInput} to the actor's input queue. - * @param inputs - The batch of inputs to add to the actor's input queue. - * @returns Promise resolving when the batch of inputs is added to the input queue. - */ - addInputs(inputs: ActorInput[]): Promise; +export interface ActorScope { + actorId: number; + userId: number; + conversationId: number; } /** - * The event map for the actor client. + * A batch of actor inputs in one request. */ -export interface ActorClientEventMap { - /** - * Emit {@link ActorMessageEvent} when the actor has processed the input and generated the output. - */ - output: ActorMessageEvent[]; -} +export type ActorInputs = InputContent[]; /** - * The input to the actor, including text, image, audio, video, etc. + * The status of the actor. */ -export type ActorInput = ActorTextInput; +export type ActorStatus = "preparing" | "running" | "idle"; /** - * The text input to the actor. + * A message from the actor. */ -export interface ActorTextInput { - /** - * The kind of the input. - */ - kind: "text"; - /** - * The content of the input. - */ +export interface ActorMessageEvent { + /** The kind of the event. */ + kind: "message"; + /** The content of the message. */ content: string; } /** - * A event from the actor. + * An event forwarded from the agent. */ -export type ActorEvent = ActorMessageEvent; +export interface ActorAgentEvent { + /** The kind of the event. */ + kind: AgentEventName; + /** The content of the message. */ + content: AgentEventUnion; +} /** - * A message from the actor. + * The event map for actor events. */ -export interface ActorMessageEvent { - /** - * The kind of the event. - */ - kind: "message"; - /** - * The content of the message. - */ - content: string; +export interface ActorEventMap { + message: [ActorMessageEvent]; + agent: [ActorAgentEvent]; } -/** Following are extended friendly typings. */ +/** + * 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; /** - * The event source for the actor client. + * 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 { /** - * Subscribes to the actor's output ({@link ActorEvent}). - * @param event - The event to subscribe to. - * @param callback - The callback to call when the event is emitted. - * @returns The actor client. - */ - on(event: "output", callback: (event: ActorMessageEvent) => void): this; - /** - * Unsubscribes from the actor's output ({@link ActorEvent}). - * @param event - The event to unsubscribe from. - * @param callback - The callback to unsubscribe from. - * @returns The actor client. + * Event emitter for actor events. */ - off(event: "output", callback: (event: ActorMessageEvent) => void): this; + readonly events: ActorEventsEmitter; /** - * Subscribes to the actor's output ({@link ActorEvent}) once. - * @param event - The event to subscribe to. - * @param callback - The callback to call when the event is emitted. - * @returns The actor client. + * 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. */ - once(event: "output", callback: (event: ActorMessageEvent) => void): this; + work(inputs: ActorInputs, addToBuffer?: boolean): Promise; /** - * Emits events from the actor. - * @param events - The events to emit. - * @returns True if the event was emitted, false otherwise. + * Reports whether the actor is currently preparing or running. */ - emit(event: "output", ...events: ActorMessageEvent[]): boolean; + isBusy(): boolean; } - -import type { Expect, Is } from "../types"; - -type Cases = [ - // EventEmitter must be a subtype of ActorEventSource. - Expect, ActorEventSource>>, -]; 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 index c02b74dc..f99f3bea 100644 --- a/packages/ema/src/concept/index.ts +++ b/packages/ema/src/concept/index.ts @@ -1,67 +1,52 @@ /** - * This module defines the concept of the EverMemoryArchive. + * Conceptual architecture of EverMemoryArchive (aligned with memory runtime design). * - * - UI is the user interface. Users can interacts with ema using WebUI, NapCatQQ, or TUI. - * - Ema Actor is the actor that takes user inputs and generates outputs. - * - Visit an actor instance using {@link ActorClient}, implemented by `ActorHttpClient` and `ActorWorker`. - * - In frontend, an `ActorHttpClient` connects to some actor in remote server. - * - In frontend or backend, an `ActorWorker` uses LLM to implement the actor logic. - * - LLM is the LLM that is responsible for the generation of the response. - * - Visit LLM providers using {@link EmaLLMClient}. - * - Create a stateful agent by extending {@link Agent}. - * - Task is the unit of work that can be scheduled and run. A task can implements one or multiple interfaces: - * - Run an agent task with {@link AgentTaskScheduler} by providing {@link AgentTask}. - * - Run a cron task with {@link TimedTaskScheduler} by providing {@link TimedTask}. - * - Storage is the storage that is responsible for the storage of the data. + * 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 - * %% UI Layer - * subgraph ui_layer ["UI Layer"] - * direction TB - * WebUI[Web UI] - * NapCat[NapCatQQ] - * TUI[Terminal UI] - * end + * subgraph UI["UI Layer"] + * WebUI["Web UI"] + * Clients["Other Clients"] + * end * - * %% Ema Actor - * Actor[Ema Actor] + * subgraph Runtime["EMA Runtime"] + * Server["Server"] + * Actor["ActorWorker"] + * Agent["Agent (LLM + Tools Loop)"] + * Memory["MemoryManager"] + * Scheduler["Scheduler"] + * end * - * %% Storage Layer - * subgraph storage_group ["Storage Layer"] - * direction TB - * MongoDB[MongoDB] - * LanceDB[LanceDB] - * end + * subgraph Storage["Storage Layer"] + * Mongo["MongoDB"] + * Lance["LanceDB (Vector Search)"] + * end * - * %% LLM Layer - * subgraph llm_group ["LLM Layer"] - * direction TB - * OpenAI[OpenAI] - * Google[Google GenAI] - * end + * subgraph LLM["LLM Providers"] + * OpenAI["OpenAI-Compatible"] + * Google["Google GenAI"] + * end * - * %% Relationships - vertical flow - * ui_layer <--> Actor - * Actor --> storage_group - * Actor --> llm_group + * UI --> Server + * Server --> Actor + * Actor --> Agent + * Agent --> LLM + * Actor --> Memory + * Memory --> Storage + * Server --> Scheduler + * Scheduler --> Actor * ``` * * @module @internals/concept */ -export { type ActorClient } from "./actor"; export * from "./actor"; - -export type { EmaLLMClient, Agent } from "./llm"; export * from "./llm"; - export * from "./storage"; - -export type { - AgentTask, - AgentTaskScheduler, - TimedTask, - TimedTaskScheduler, -} from "./task"; -export * from "./task"; diff --git a/packages/ema/src/concept/llm.ts b/packages/ema/src/concept/llm.ts index ba90d4ad..cb01dcfa 100644 --- a/packages/ema/src/concept/llm.ts +++ b/packages/ema/src/concept/llm.ts @@ -1,171 +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"; /** - * {@link EmaLLMClient} is a stateless client for the LLM, holding a physical network connection to the LLM. - * - * TODO: remove mini-agent's LLMClient definition. + * LLM providers supported by EMA runtime. */ -export interface EmaLLMClient { +export enum LLMProvider { + GOOGLE = "google", + ANTHROPIC = "anthropic", + OPENAI = "openai", +} + +/** + * Stateless LLM client abstraction used by agent runtime. + */ +export declare class LLMClient { /** - * Generates response from LLM. + * Generates one model turn from current context. * - * @param messages - List of conversation messages - * @param tools - Optional list of Tool objects or dicts - * @param systemPrompt - Optional system prompt to start with - * @returns LLMResponse containing the generated content, thinking, and tool calls + * @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; } -// TODO: definition of tools. -export type Tool = any; +/** 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; /** - * The state of the agent. - * More state could be added for specific agents, e.g. `memoryBuffer` for agents who have long-term memory. + * Input data for scheduling a job. */ -export interface AgentState { +export interface JobSpec { /** - * The system prompt of the agent. + * The job name used to resolve a handler. */ - systemPrompt: string; + name: K; /** - * The messages of the agent to start with. + * When the job should run (Unix timestamp in milliseconds). */ - messages: Message[]; + runAt: number; /** - * The tools of the agent. + * Handler-specific data. */ - tools: Tool[]; + data: JobData; } -export interface MutatableAgentState extends AgentState { +/** + * Input data for scheduling a recurring job. + */ +export interface JobEverySpec { + /** + * The job name used to resolve a handler. + */ + name: K; /** - * Replaces the state with a new state. - * @param s - The state to replace with. + * Earliest time the recurring schedule becomes active (Unix timestamp in milliseconds). */ - replace(s: AgentState): void; + 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; } /** - * The state callback of the agent. You can visit the state in the callback, - * and call the `next` function to continue to run the next callback. - * - * - The next function can only be called once. - * - If the next is not called, the agent will keep state change but will not run. - * - * @param state - The state of the agent. - * @param next - The next function to call. - * @returns The state of the agent. - * - * @example - * ```ts - * // Runs with additional messages. - * const agent = new AgentImpl(); - * agent.run(async (state, next) => { - * state.history.push({ type: "user", content: "Hello, World!" }); - * await next(); - * return state; - * }); - * ``` - * - * @example - * ```ts - * // Runs without saving history - * const agent = new AgentImpl(); - * agent.run(async (state, next) => { - * const messages = state.history; - * state.history.push({ type: "user", content: "Hello, World!" }); - * await next(); - * state.history = messages; - * return state; - * }); - * ``` + * Scheduler job handler signature. */ -export type AgentStateCallback = ( - state: S & MutatableAgentState, - next: () => Promise, -) => Promise; +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; +} /** - * {@link Agent} is a background-running thread that communicates with the actor. + * Scheduler interface for managing job lifecycle. */ -export abstract class Agent { +export interface Scheduler { /** - * The event source of the agent. See {@link AgentEventSource} for more details. + * Starts the scheduler loop. + * @param handlers - Mapping of job names to their handlers. + * @returns Promise resolving when the scheduler is started. */ - abstract events: EventEmitter & AgentEventSource; - + start(handlers: JobHandlerMap): Promise; /** - * Checks if the agent is running a LLM session. - * - * @returns Whether the agent is running. + * Stops the scheduler loop. + * @returns Promise resolving when the scheduler is stopped. */ - abstract isRunning(): boolean; + stop(): Promise; /** - * Stops the running session unconditionally. - * + * Schedules a job for execution. + * @param job - The job to schedule. + * @returns Promise resolving to the job id. */ - abstract stop(): Promise; - + schedule(job: JobSpec): Promise; /** - * Runs the agent in stateless manner. - * - * @param state - The state to run the agent with. - * @returns Promise resolving when the agent is idle. + * 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. */ - execute(state: S): Promise { - return this.run(async (s, next) => { - s.replace(state); - await next(); - return s; - }); - } - + reschedule(id: JobId, job: JobSpec): Promise; /** - * Runs the agent with an additional message in OpenAI format. - * - * @param message - The message to add. - * @returns Promise resolving when the agent is idle. + * Cancels a pending job by id. + * @param id - The job identifier. + * @returns Promise resolving to true if canceled, false otherwise. */ - runWithMessage(message: Message): Promise { - return this.run(async (s, next) => { - s.messages.push(message); - await next(); - return s; - }); - } - + cancel(id: JobId): Promise; /** - * Runs a state callback when the agent becomes idle. If the agent is running a LLM session, the callback will be - * called after the session is finished. - * - * See {@link AgentStateCallback} for examples. - * - * @param stateCallback - The state callback to run the agent with. - * @returns Promise resolving when the agent is idle. + * Schedules a recurring job. + * @param job - The recurring job data. + * @returns Promise resolving to the job id. */ - abstract run(stateCallback: AgentStateCallback): Promise; + 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; } /** - * The event map for the agent. + * Mapping of job names to their handlers. */ -export interface AgentEventMap { - // todo: agent events. maybe: - // 1. agent output - // 2. special agent event, for example, we can define "memory:reorganize" to tell other components the agent is - // reorganizing memory. -} - -/** Following are extended friendly typings. */ +export type JobHandlerMap = Partial<{ + [K in JobName]: JobHandler; +}>; /** - * The event source for the agent. + * Runtime status of the scheduler. */ -export interface AgentEventSource {} +export type SchedulerStatus = "idle" | "running" | "stopping"; diff --git a/packages/ema/src/concept/storage.ts b/packages/ema/src/concept/storage.ts index 9bf18ea6..ba1b2482 100644 --- a/packages/ema/src/concept/storage.ts +++ b/packages/ema/src/concept/storage.ts @@ -1,84 +1,165 @@ -import type { Message } from "../schema"; +import type { Content as InputContent } from "../schema"; /** - * Interface for persisting actor state + * 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(): Promise; - /** - * Updates the state of the actor - * @param state - The state to update - * @returns Promise resolving when the state is updated - */ - updateState(state: ActorState): Promise; + getState(actorId: number, conversationId: number): Promise; } +/** + * Runtime state for an actor. + */ export interface ActorState { /** - * The memory buffer, in the format of messages in OpenAI chat completion API. + * The lastest short-term memory for the actor. */ - - memoryBuffer: Message[]; - // more state can be added here. + memoryDay: ShortTermMemory; + memoryWeek: ShortTermMemory; + memoryMonth: ShortTermMemory; + memoryYear: ShortTermMemory; + /** + * The buffer messages for the actor. + */ + buffer: BufferMessage[]; } /** - * Interface for actor memory + * Interface for actor memory. */ export interface ActorMemory { /** * Searches actor memory - * @param keywords - Keywords to search for + * @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(keywords: string[]): Promise; + 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(item: ShortTermMemory): Promise; + 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(item: LongTermMemory): Promise; + addLongTermMemory(actorId: number, item: LongTermMemory): Promise; } /** - * Result of searching agent memory + * Short-term memory item captured at a specific granularity. */ -export interface SearchActorMemoryResult { - /** - * The long term memories found - */ - items: LongTermMemory[]; -} - export interface ShortTermMemory { /** - * The granularity of short term memory + * The granularity of short term memory. */ - kind: "year" | "month" | "day"; + kind: "year" | "month" | "week" | "day"; /** - * The os when the actor saw the messages. + * The memory text when the actor saw the messages. */ - os: string; + memory: string; /** - * The statement when the actor saw the messages. + * The date and time the memory was created. */ - statement: string; + createdAt?: number; /** - * The date and time the memory was created + * Related conversation message IDs for traceability. */ - createdAt: number; + 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. 一级分类 @@ -89,19 +170,25 @@ export interface LongTermMemory { */ index1: string; /** - * The keywords to search + * The memory text when the actor saw the messages. */ - keywords: string[]; + memory: string; /** - * The os when the actor saw the messages. + * The date and time the memory was created */ - os: string; + createdAt?: number; /** - * The statement when the actor saw the messages. + * Related conversation message IDs for traceability. */ - statement: string; + messages?: number[]; +} + +/** + * Long-term memory record with identifier. + */ +export type LongTermMemoryRecord = LongTermMemory & { /** - * The date and time the memory was created + * The unique identifier for the memory record. */ - createdAt: number; -} + id: number; +}; diff --git a/scripts/docs.js b/scripts/docs.js index 28a61e6c..9887c671 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -8,7 +8,6 @@ function docsGen() { const entryFlag = (it) => `--entryPoints ${it}`; const coreEntries = [ - "packages/ema/src/index.ts", "packages/ema/src/config.ts", "packages/ema/src/db/index.ts", "packages/ema/src/concept/index.ts", From 67a04eedc45566928087466241d3ae18f4b49876 Mon Sep 17 00:00:00 2001 From: Disviel Date: Sun, 1 Mar 2026 22:39:59 +0800 Subject: [PATCH 29/29] fix: align concept task typing with non-generic agent --- packages/ema/src/concept/task.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ema/src/concept/task.ts b/packages/ema/src/concept/task.ts index 62315e33..3d38bd5c 100644 --- a/packages/ema/src/concept/task.ts +++ b/packages/ema/src/concept/task.ts @@ -1,4 +1,4 @@ -import type { Agent, AgentState } from "./llm"; +import type { Agent } from "./llm"; /** * A task is a unit of work that can be scheduled and run. @@ -35,12 +35,12 @@ export interface Task { * scheduler.schedule(dailyTask); * ``` */ -export interface AgentTask extends Task { +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; + agent?: Agent; /** * Runs the task with the agent and schedule context. @@ -49,7 +49,7 @@ export interface AgentTask extends Task { * @param scheduler - The scheduler to run the task with. * @returns Promise resolving when the task is completed. */ - work(agent: Agent, scheduler: AgentTaskScheduler): Promise; + work(agent: Agent, scheduler: AgentTaskScheduler): Promise; } /**