emocentric 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/actions/action.d.ts +70 -0
- package/dist/actions/action.js +47 -0
- package/dist/actions/index.d.ts +2 -0
- package/dist/actions/index.js +2 -0
- package/dist/actions/registry.d.ts +12 -0
- package/dist/actions/registry.js +26 -0
- package/dist/agent.d.ts +141 -0
- package/dist/agent.js +246 -0
- package/dist/blueprint/blueprint.d.ts +60 -0
- package/dist/blueprint/blueprint.js +20 -0
- package/dist/blueprint/index.d.ts +2 -0
- package/dist/blueprint/index.js +2 -0
- package/dist/blueprint/scope.d.ts +26 -0
- package/dist/blueprint/scope.js +21 -0
- package/dist/emotion/definitions.d.ts +34 -0
- package/dist/emotion/definitions.js +43 -0
- package/dist/emotion/index.d.ts +3 -0
- package/dist/emotion/index.js +3 -0
- package/dist/emotion/persona.d.ts +35 -0
- package/dist/emotion/persona.js +22 -0
- package/dist/emotion/state.d.ts +52 -0
- package/dist/emotion/state.js +35 -0
- package/dist/events/context-event.d.ts +49 -0
- package/dist/events/context-event.js +43 -0
- package/dist/events/guards.d.ts +14 -0
- package/dist/events/guards.js +72 -0
- package/dist/events/index.d.ts +8 -0
- package/dist/events/index.js +5 -0
- package/dist/events/media.d.ts +23 -0
- package/dist/events/media.js +16 -0
- package/dist/events/merge.d.ts +24 -0
- package/dist/events/merge.js +61 -0
- package/dist/events/payload.d.ts +54 -0
- package/dist/events/payload.js +8 -0
- package/dist/gateway/client.d.ts +28 -0
- package/dist/gateway/client.js +79 -0
- package/dist/gateway/index.d.ts +1 -0
- package/dist/gateway/index.js +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +13 -0
- package/dist/llm/index.d.ts +1 -0
- package/dist/llm/index.js +1 -0
- package/dist/llm/types.d.ts +40 -0
- package/dist/llm/types.js +1 -0
- package/dist/memory/fact.d.ts +54 -0
- package/dist/memory/fact.js +1 -0
- package/dist/memory/index.d.ts +4 -0
- package/dist/memory/index.js +2 -0
- package/dist/memory/openai-embedder.d.ts +50 -0
- package/dist/memory/openai-embedder.js +124 -0
- package/dist/memory/scoring.d.ts +24 -0
- package/dist/memory/scoring.js +20 -0
- package/dist/memory/store.d.ts +79 -0
- package/dist/memory/store.js +1 -0
- package/dist/openrouter/client.d.ts +27 -0
- package/dist/openrouter/client.js +118 -0
- package/dist/openrouter/index.d.ts +1 -0
- package/dist/openrouter/index.js +1 -0
- package/dist/pipeline/actor.d.ts +31 -0
- package/dist/pipeline/actor.js +11 -0
- package/dist/pipeline/appraiser.d.ts +55 -0
- package/dist/pipeline/appraiser.js +132 -0
- package/dist/pipeline/consolidator.d.ts +62 -0
- package/dist/pipeline/consolidator.js +98 -0
- package/dist/pipeline/inbox.d.ts +60 -0
- package/dist/pipeline/inbox.js +107 -0
- package/dist/pipeline/index.d.ts +11 -0
- package/dist/pipeline/index.js +11 -0
- package/dist/pipeline/interpretation.d.ts +47 -0
- package/dist/pipeline/interpretation.js +24 -0
- package/dist/pipeline/llm-interpreter.d.ts +28 -0
- package/dist/pipeline/llm-interpreter.js +173 -0
- package/dist/pipeline/loop.d.ts +58 -0
- package/dist/pipeline/loop.js +73 -0
- package/dist/pipeline/processor.d.ts +63 -0
- package/dist/pipeline/processor.js +144 -0
- package/dist/pipeline/recaller.d.ts +34 -0
- package/dist/pipeline/recaller.js +38 -0
- package/dist/pipeline/time-appraiser.d.ts +85 -0
- package/dist/pipeline/time-appraiser.js +189 -0
- package/dist/pipeline/writer.d.ts +113 -0
- package/dist/pipeline/writer.js +210 -0
- package/dist/postgres/create-instance.d.ts +107 -0
- package/dist/postgres/create-instance.js +182 -0
- package/dist/postgres/emotional-state.d.ts +42 -0
- package/dist/postgres/emotional-state.js +134 -0
- package/dist/postgres/index.d.ts +5 -0
- package/dist/postgres/index.js +5 -0
- package/dist/postgres/instances.d.ts +32 -0
- package/dist/postgres/instances.js +77 -0
- package/dist/postgres/schema.d.ts +31 -0
- package/dist/postgres/schema.js +98 -0
- package/dist/postgres/short-term.d.ts +32 -0
- package/dist/postgres/short-term.js +95 -0
- package/dist/postgres/store.d.ts +85 -0
- package/dist/postgres/store.js +347 -0
- package/dist/short-term/index.d.ts +1 -0
- package/dist/short-term/index.js +1 -0
- package/dist/short-term/short-term.d.ts +66 -0
- package/dist/short-term/short-term.js +72 -0
- package/package.json +50 -0
package/dist/agent.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { InMemoryShortTermMemory, } from "./short-term/short-term.js";
|
|
2
|
+
import { ActionRegistry } from "./actions/registry.js";
|
|
3
|
+
import { ActionPlan } from "./actions/action.js";
|
|
4
|
+
import { LLMInterpreter } from "./pipeline/llm-interpreter.js";
|
|
5
|
+
import { MemoryRecaller } from "./pipeline/recaller.js";
|
|
6
|
+
import { InterpretRecallLoop, } from "./pipeline/loop.js";
|
|
7
|
+
import { LLMAppraiser, } from "./pipeline/appraiser.js";
|
|
8
|
+
import { DecayTimeAppraiser, } from "./pipeline/time-appraiser.js";
|
|
9
|
+
import { LLMWriter } from "./pipeline/writer.js";
|
|
10
|
+
import { LLMProcessor, } from "./pipeline/processor.js";
|
|
11
|
+
import { DefaultActor } from "./pipeline/actor.js";
|
|
12
|
+
import { LLMConsolidator, } from "./pipeline/consolidator.js";
|
|
13
|
+
import { InstanceInbox, HeartbeatCancelled, } from "./pipeline/inbox.js";
|
|
14
|
+
/** Wraps a stage failure with which stage broke; the original error is on
|
|
15
|
+
* `cause`. Bad LLM *output* never raises this (stages degrade to no-ops) —
|
|
16
|
+
* this is for genuine failures: LLM outage, database down, bugs. */
|
|
17
|
+
export class AgentError extends Error {
|
|
18
|
+
stage;
|
|
19
|
+
constructor(stage, cause) {
|
|
20
|
+
const detail = cause instanceof Error ? cause.message : String(cause);
|
|
21
|
+
super(`Agent stage "${stage}" failed: ${detail}`, { cause });
|
|
22
|
+
this.name = "AgentError";
|
|
23
|
+
this.stage = stage;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The orchestrator: wires the six pipeline stages into one call.
|
|
28
|
+
*
|
|
29
|
+
* handleEvent(event):
|
|
30
|
+
* 1. time appraisal feelings drift for the time since last contact
|
|
31
|
+
* 2. interpret/recall understand the event, grounded in memories
|
|
32
|
+
* 3. event appraisal feelings respond (persisted)
|
|
33
|
+
* 4. writer new facts filed, contradictions superseded
|
|
34
|
+
* 5. processor actions chosen in-feeling; plan returned, never
|
|
35
|
+
* executed — the caller runs plan.execute()
|
|
36
|
+
*
|
|
37
|
+
* Every stage is the shipped default unless overridden in options, and the
|
|
38
|
+
* developer-owned pieces (memory, emotions, actions, persona, LLM) are
|
|
39
|
+
* injected — so any layer can be rewritten without touching this class.
|
|
40
|
+
*/
|
|
41
|
+
export class Agent {
|
|
42
|
+
memory;
|
|
43
|
+
shortTermMemory;
|
|
44
|
+
emotions;
|
|
45
|
+
actions;
|
|
46
|
+
persona;
|
|
47
|
+
timeAppraiser;
|
|
48
|
+
loop;
|
|
49
|
+
appraiser;
|
|
50
|
+
writer;
|
|
51
|
+
processor;
|
|
52
|
+
actor;
|
|
53
|
+
consolidator;
|
|
54
|
+
/** Serializes + coalesces events for this agent. enqueue() is the entry;
|
|
55
|
+
* it drives handleEvent under cancellation. */
|
|
56
|
+
inbox = new InstanceInbox((event, controls) => this.handleEvent(event, {
|
|
57
|
+
signal: controls.signal,
|
|
58
|
+
onCommit: controls.onCommit,
|
|
59
|
+
}));
|
|
60
|
+
constructor(options) {
|
|
61
|
+
this.memory = options.memory;
|
|
62
|
+
this.shortTermMemory =
|
|
63
|
+
options.shortTermMemory ?? new InMemoryShortTermMemory();
|
|
64
|
+
this.emotions = options.emotions;
|
|
65
|
+
this.actions = options.actions ?? new ActionRegistry();
|
|
66
|
+
this.persona = options.persona;
|
|
67
|
+
this.timeAppraiser = options.timeAppraiser ?? new DecayTimeAppraiser();
|
|
68
|
+
const interpreter = options.interpreter ??
|
|
69
|
+
new LLMInterpreter({
|
|
70
|
+
llm: options.llm,
|
|
71
|
+
...(options.persona !== undefined && {
|
|
72
|
+
personaHint: `${options.persona.name} — ${options.persona.summary}`,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
const recaller = options.recaller ?? new MemoryRecaller({ store: options.memory });
|
|
76
|
+
this.loop =
|
|
77
|
+
options.loop ??
|
|
78
|
+
new InterpretRecallLoop({
|
|
79
|
+
interpreter,
|
|
80
|
+
recaller,
|
|
81
|
+
...(options.maxLoopIterations !== undefined && {
|
|
82
|
+
maxIterations: options.maxLoopIterations,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
this.appraiser = options.appraiser ?? new LLMAppraiser({ llm: options.llm });
|
|
86
|
+
this.writer =
|
|
87
|
+
options.writer ??
|
|
88
|
+
new LLMWriter({ llm: options.llm, store: options.memory });
|
|
89
|
+
this.processor =
|
|
90
|
+
options.processor ?? new LLMProcessor({ llm: options.llm });
|
|
91
|
+
this.actor = options.actor ?? new DefaultActor();
|
|
92
|
+
this.consolidator =
|
|
93
|
+
options.consolidator ??
|
|
94
|
+
new LLMConsolidator({ llm: options.llm, store: options.memory });
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* The production entry point: hand an event to this agent's funnel. Events
|
|
98
|
+
* for one agent process one at a time; a burst that arrives before the
|
|
99
|
+
* current heartbeat commits its feelings is merged and run as one. Resolves
|
|
100
|
+
* to the heartbeat's {@link AgentResult}, or a {@link MergedOutcome} marker
|
|
101
|
+
* when this event was folded into a later one (do nothing in that case).
|
|
102
|
+
*
|
|
103
|
+
* Prefer this over {@link handleEvent} — it is the only path that serializes
|
|
104
|
+
* and coalesces. handleEvent runs immediately and bypasses both.
|
|
105
|
+
*/
|
|
106
|
+
enqueue(event) {
|
|
107
|
+
return this.inbox.enqueue(event);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Run one heartbeat immediately, bypassing the funnel. Internal: the inbox
|
|
111
|
+
* calls this under cancellation control. Calling it directly skips
|
|
112
|
+
* serialization and coalescing — only safe when you guarantee no concurrent
|
|
113
|
+
* events for this agent. Most callers want {@link enqueue}.
|
|
114
|
+
*/
|
|
115
|
+
async handleEvent(event, options = {}) {
|
|
116
|
+
// Working memory: the recent window the interpreter, processor, and
|
|
117
|
+
// time-appraiser reason over. Loaded first; the first interpret pass is
|
|
118
|
+
// still neutral (the loop withholds it), and this event is appended at the
|
|
119
|
+
// end of the turn.
|
|
120
|
+
const recent = await this.shortTermMemory.recent();
|
|
121
|
+
cancelPoint(options.signal);
|
|
122
|
+
const interpretation = await this.stage("interpretation", () => this.loop.run(event, recent));
|
|
123
|
+
const facts = interpretation.memories.map((m) => m.fact);
|
|
124
|
+
// Last cancellable point: past here, feelings persist and the event is
|
|
125
|
+
// committed. onCommit() and the appraisal must follow with NO await
|
|
126
|
+
// between them and this checkpoint, or the cancel/commit race opens up.
|
|
127
|
+
cancelPoint(options.signal);
|
|
128
|
+
options.onCommit?.();
|
|
129
|
+
// Time passes: feelings catch up to `now`, reasoning over the recent
|
|
130
|
+
// thread. Deliberately PAST the commit point so an LLM time-appraiser
|
|
131
|
+
// fires exactly once per committed heartbeat — a coalesced/cancelled
|
|
132
|
+
// heartbeat never reaches here. The interpreter doesn't use feelings, so
|
|
133
|
+
// running decay here (instead of first) is invisible to it; the appraiser
|
|
134
|
+
// and processor below still see post-decay feelings. Math decay is
|
|
135
|
+
// unaffected.
|
|
136
|
+
const time = await this.stage("time-appraisal", () => this.timeAppraiser.appraise({
|
|
137
|
+
state: this.emotions,
|
|
138
|
+
now: event.occurredAt,
|
|
139
|
+
event,
|
|
140
|
+
recent,
|
|
141
|
+
}));
|
|
142
|
+
const appraisal = await this.stage("appraisal", () => this.appraiser.appraise({
|
|
143
|
+
event,
|
|
144
|
+
description: interpretation.description,
|
|
145
|
+
memories: facts,
|
|
146
|
+
state: this.emotions,
|
|
147
|
+
...(this.persona !== undefined && { persona: this.persona }),
|
|
148
|
+
}));
|
|
149
|
+
const memory = await this.stage("write", () => this.writer.write({
|
|
150
|
+
event,
|
|
151
|
+
description: interpretation.description,
|
|
152
|
+
memories: facts,
|
|
153
|
+
appraisal,
|
|
154
|
+
...(this.persona !== undefined && { persona: this.persona }),
|
|
155
|
+
}));
|
|
156
|
+
const actions = await this.stage("process", async () => {
|
|
157
|
+
if (this.actions.size === 0) {
|
|
158
|
+
// No menu — skip the LLM call entirely; an empty plan is the answer.
|
|
159
|
+
return {
|
|
160
|
+
plan: new ActionPlan([], [], [], {
|
|
161
|
+
event,
|
|
162
|
+
description: interpretation.description,
|
|
163
|
+
memories: facts,
|
|
164
|
+
emotions: appraisal.after,
|
|
165
|
+
...(this.persona !== undefined && { persona: this.persona }),
|
|
166
|
+
}),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return this.processor.process({
|
|
170
|
+
event,
|
|
171
|
+
description: interpretation.description,
|
|
172
|
+
memories: facts,
|
|
173
|
+
state: this.emotions,
|
|
174
|
+
registry: this.actions,
|
|
175
|
+
recent,
|
|
176
|
+
...(this.persona !== undefined && { persona: this.persona }),
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
// Past the commit point: the agent now carries out its plan (side effects
|
|
180
|
+
// and all) exactly once. A merged/cancelled heartbeat never reaches here,
|
|
181
|
+
// so the agent never acts on an event that was folded into a later one.
|
|
182
|
+
const acted = await this.stage("act", () => this.actor.act({ plan: actions.plan }));
|
|
183
|
+
// Remember what the agent itself just did (met someone, made a plan), so
|
|
184
|
+
// its own actions feed back into memory — not only what others did to it.
|
|
185
|
+
const consolidation = await this.stage("consolidate", () => this.consolidator.consolidate({
|
|
186
|
+
event,
|
|
187
|
+
executions: acted.executions,
|
|
188
|
+
description: interpretation.description,
|
|
189
|
+
memories: facts,
|
|
190
|
+
...(this.persona !== undefined && { persona: this.persona }),
|
|
191
|
+
}));
|
|
192
|
+
// Close the loop: drop this engagement into the short-term window so the
|
|
193
|
+
// next event's interpreter/processor see it. Best-effort — a bookkeeping
|
|
194
|
+
// failure must not fail a turn whose action already happened.
|
|
195
|
+
await this.remember(event, interpretation.description, acted.executions);
|
|
196
|
+
return {
|
|
197
|
+
event,
|
|
198
|
+
shortTerm: recent,
|
|
199
|
+
time,
|
|
200
|
+
interpretation,
|
|
201
|
+
appraisal,
|
|
202
|
+
memory,
|
|
203
|
+
actions,
|
|
204
|
+
acted,
|
|
205
|
+
consolidation,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/** Record one engagement in short-term memory: what the agent perceived and
|
|
209
|
+
* the words it actually said back (joined; empty when it stayed silent). */
|
|
210
|
+
async remember(event, perceived, executions) {
|
|
211
|
+
const responded = executions
|
|
212
|
+
.filter((e) => e.status === "succeeded" &&
|
|
213
|
+
typeof e.result === "string" &&
|
|
214
|
+
e.result.trim() !== "")
|
|
215
|
+
.map((e) => e.result.trim())
|
|
216
|
+
.join(" ");
|
|
217
|
+
try {
|
|
218
|
+
await this.shortTermMemory.append({
|
|
219
|
+
occurredAt: event.occurredAt,
|
|
220
|
+
actor: event.actor.displayName !== undefined
|
|
221
|
+
? { id: event.actor.id, displayName: event.actor.displayName }
|
|
222
|
+
: { id: event.actor.id },
|
|
223
|
+
perceived,
|
|
224
|
+
responded,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
// Best-effort working memory; swallow.
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async stage(name, run) {
|
|
232
|
+
try {
|
|
233
|
+
return await run();
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
throw new AgentError(name, error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Throw the merge sentinel if the inbox has asked to cancel. A no-op for a
|
|
241
|
+
* plain handleEvent call (no signal). Kept outside `stage()` so cancellation
|
|
242
|
+
* is never mistaken for a stage failure. */
|
|
243
|
+
function cancelPoint(signal) {
|
|
244
|
+
if (signal?.aborted)
|
|
245
|
+
throw new HeartbeatCancelled();
|
|
246
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Persona } from "../emotion/persona.js";
|
|
2
|
+
import type { EmotionDefinition } from "../emotion/definitions.js";
|
|
3
|
+
import type { FactInput } from "../memory/fact.js";
|
|
4
|
+
import type { ActionDefinition } from "../actions/action.js";
|
|
5
|
+
import { ActionRegistry } from "../actions/registry.js";
|
|
6
|
+
/**
|
|
7
|
+
* A declarative action: everything the LLM needs to choose it (name,
|
|
8
|
+
* description, slot, params), with the side effect left out. The blueprint
|
|
9
|
+
* says WHAT the agent can do and when; the platform supplies HOW it
|
|
10
|
+
* physically happens (an ActionExecutor) at instance creation.
|
|
11
|
+
*/
|
|
12
|
+
export interface ActionBlueprint {
|
|
13
|
+
name: string;
|
|
14
|
+
/** For the LLM: what the action does AND when to choose it. */
|
|
15
|
+
description: string;
|
|
16
|
+
/** Mutually-exclusive group; empty/undefined combines freely. */
|
|
17
|
+
slot?: string;
|
|
18
|
+
/** JSON schema for the arguments the LLM must fill. */
|
|
19
|
+
paramsSchema?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
/** The code half of an ActionBlueprint — the platform's side effect. */
|
|
22
|
+
export type ActionExecutor = ActionDefinition["act"];
|
|
23
|
+
/**
|
|
24
|
+
* The design-time definition of an agent, authored in the studio and stored
|
|
25
|
+
* as plain JSON. A blueprint is a recipe, not a being: it never feels or
|
|
26
|
+
* remembers anything itself. Users get independent instances cooked from it,
|
|
27
|
+
* each born with the blueprint's starting emotions and seed memories and
|
|
28
|
+
* diverging from there.
|
|
29
|
+
*
|
|
30
|
+
* Seed facts are copied at birth, not referenced — editing a blueprint later
|
|
31
|
+
* affects only new instances; existing ones keep their lived history.
|
|
32
|
+
*/
|
|
33
|
+
export interface AgentBlueprint {
|
|
34
|
+
/** Stable identifier, e.g. "lyra". Becomes `InstanceScope.blueprintId`. */
|
|
35
|
+
id: string;
|
|
36
|
+
/** Studio iteration counter; instances record the version they were born
|
|
37
|
+
* from. */
|
|
38
|
+
version?: number;
|
|
39
|
+
/** Display name. Defaults to `persona.name` wherever one is needed. */
|
|
40
|
+
name?: string;
|
|
41
|
+
/** Display avatar (URL or data URI). */
|
|
42
|
+
avatar?: string;
|
|
43
|
+
persona: Persona;
|
|
44
|
+
/** Custom emotion palette; omit for the SDK default. */
|
|
45
|
+
emotions?: EmotionDefinition[];
|
|
46
|
+
/** The action menu, declaratively. Omit for an agent that only perceives,
|
|
47
|
+
* feels, and remembers. */
|
|
48
|
+
actions?: ActionBlueprint[];
|
|
49
|
+
/** Memories every instance is born with — written through the normal
|
|
50
|
+
* MemoryStore on first contact, then indistinguishable from lived ones. */
|
|
51
|
+
seedFacts?: FactInput[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Compile a blueprint's declarative actions into a runnable registry by
|
|
55
|
+
* pairing each one with the platform's executor of the same name. Throws if
|
|
56
|
+
* an action has no executor (a wiring bug — the LLM could choose an action
|
|
57
|
+
* nobody implements). Extra executors are ignored, so one platform-wide map
|
|
58
|
+
* serves blueprints with different menus.
|
|
59
|
+
*/
|
|
60
|
+
export declare function compileActions(actions: readonly ActionBlueprint[] | undefined, executors?: Record<string, ActionExecutor>): ActionRegistry;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ActionRegistry } from "../actions/registry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Compile a blueprint's declarative actions into a runnable registry by
|
|
4
|
+
* pairing each one with the platform's executor of the same name. Throws if
|
|
5
|
+
* an action has no executor (a wiring bug — the LLM could choose an action
|
|
6
|
+
* nobody implements). Extra executors are ignored, so one platform-wide map
|
|
7
|
+
* serves blueprints with different menus.
|
|
8
|
+
*/
|
|
9
|
+
export function compileActions(actions, executors = {}) {
|
|
10
|
+
const registry = new ActionRegistry();
|
|
11
|
+
for (const action of actions ?? []) {
|
|
12
|
+
const act = executors[action.name];
|
|
13
|
+
if (act === undefined) {
|
|
14
|
+
throw new Error(`No executor provided for blueprint action "${action.name}". ` +
|
|
15
|
+
"Every declared action needs a matching entry in `executors`.");
|
|
16
|
+
}
|
|
17
|
+
registry.register({ ...action, act });
|
|
18
|
+
}
|
|
19
|
+
return registry;
|
|
20
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity of one agent instance: which blueprint it was born from and whose
|
|
3
|
+
* instance it is. An agent is never shared across users — each user gets an
|
|
4
|
+
* independent instance with its own memories and feelings.
|
|
5
|
+
*
|
|
6
|
+
* `sessionId` is reserved for the future case of several people engaging one
|
|
7
|
+
* shared instance (group chats, rooms). Carrying it in the scope now means
|
|
8
|
+
* adding sessions later changes no storage keys or schemas — the key simply
|
|
9
|
+
* grows a segment.
|
|
10
|
+
*/
|
|
11
|
+
export interface InstanceScope {
|
|
12
|
+
/** Which blueprint (character) this instance was born from. */
|
|
13
|
+
blueprintId: string;
|
|
14
|
+
/** The user this instance belongs to. */
|
|
15
|
+
userId: string;
|
|
16
|
+
/** Reserved: scopes the instance to one session/room instead of one
|
|
17
|
+
* user's private copy. Omit for the default one-user-one-instance model. */
|
|
18
|
+
sessionId?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Canonical storage key for a scope — `"lyra/user-42"`, or
|
|
22
|
+
* `"lyra/user-42/sess-7"` once sessions exist. Every persisted row is keyed
|
|
23
|
+
* by this string. Segments are URI-encoded so an id containing "/" can never
|
|
24
|
+
* collide with another scope's key.
|
|
25
|
+
*/
|
|
26
|
+
export declare function instanceKey(scope: InstanceScope): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical storage key for a scope — `"lyra/user-42"`, or
|
|
3
|
+
* `"lyra/user-42/sess-7"` once sessions exist. Every persisted row is keyed
|
|
4
|
+
* by this string. Segments are URI-encoded so an id containing "/" can never
|
|
5
|
+
* collide with another scope's key.
|
|
6
|
+
*/
|
|
7
|
+
export function instanceKey(scope) {
|
|
8
|
+
requireSegment("blueprintId", scope.blueprintId);
|
|
9
|
+
requireSegment("userId", scope.userId);
|
|
10
|
+
const segments = [scope.blueprintId, scope.userId];
|
|
11
|
+
if (scope.sessionId !== undefined) {
|
|
12
|
+
requireSegment("sessionId", scope.sessionId);
|
|
13
|
+
segments.push(scope.sessionId);
|
|
14
|
+
}
|
|
15
|
+
return segments.map(encodeURIComponent).join("/");
|
|
16
|
+
}
|
|
17
|
+
function requireSegment(field, value) {
|
|
18
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
19
|
+
throw new Error(`InstanceScope.${field} must be a non-empty string.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** One feeling slider in the agent's palette. */
|
|
2
|
+
export interface EmotionDefinition {
|
|
3
|
+
/** Identity of the slider — the key used in snapshots and deltas. */
|
|
4
|
+
name: string;
|
|
5
|
+
/** Written FOR THE LLM: the appraiser judges movement of this slider
|
|
6
|
+
* against this description, so make it agent-meaningful. */
|
|
7
|
+
description: string;
|
|
8
|
+
/** 0..1 — starting value, and (later) the value time decays back toward. */
|
|
9
|
+
baseline: number;
|
|
10
|
+
/** Gain applied to incoming deltas (1 = neutral). 1.8 on anger makes a
|
|
11
|
+
* hot-headed agent; 0.5 on trust makes one slow to warm up. Usually set
|
|
12
|
+
* via Persona temperament rather than here. */
|
|
13
|
+
sensitivity?: number;
|
|
14
|
+
/** Time for this emotion's distance-from-baseline to halve during the
|
|
15
|
+
* time-appraisal stage. Unset = the time appraiser's default (4h);
|
|
16
|
+
* Infinity = never decays. Usually set via Persona temperament. */
|
|
17
|
+
halfLifeMinutes?: number;
|
|
18
|
+
}
|
|
19
|
+
/** Current values of every slider: { warmth: 0.55, anger: 0.12, ... } */
|
|
20
|
+
export type EmotionSnapshot = Record<string, number>;
|
|
21
|
+
/** One appraised movement of one slider. `delta` is -1..+1, BEFORE
|
|
22
|
+
* sensitivity scaling. `reason` is for debugging and expressiveness. */
|
|
23
|
+
export interface EmotionDelta {
|
|
24
|
+
emotion: string;
|
|
25
|
+
delta: number;
|
|
26
|
+
reason?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The engine's default palette. Use as-is, filter some out, add your own,
|
|
30
|
+
* or replace entirely — it's just an array:
|
|
31
|
+
*
|
|
32
|
+
* [...DEFAULT_EMOTIONS.filter(e => e.name !== "fear"), { name: "pride", ... }]
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFAULT_EMOTIONS: readonly EmotionDefinition[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's default palette. Use as-is, filter some out, add your own,
|
|
3
|
+
* or replace entirely — it's just an array:
|
|
4
|
+
*
|
|
5
|
+
* [...DEFAULT_EMOTIONS.filter(e => e.name !== "fear"), { name: "pride", ... }]
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_EMOTIONS = [
|
|
8
|
+
{
|
|
9
|
+
name: "warmth",
|
|
10
|
+
description: "affection, care, and fondness toward the person currently interacting",
|
|
11
|
+
baseline: 0.5,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "joy",
|
|
15
|
+
description: "delight, amusement, playfulness, lightness",
|
|
16
|
+
baseline: 0.4,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "sadness",
|
|
20
|
+
description: "loss, disappointment, melancholy, missing something",
|
|
21
|
+
baseline: 0.2,
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "anger",
|
|
25
|
+
description: "feeling wronged or disrespected, friction, indignation, frustration",
|
|
26
|
+
baseline: 0.1,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: "fear",
|
|
30
|
+
description: "anxiety, sense of threat, dread, worry about what may happen",
|
|
31
|
+
baseline: 0.15,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "trust",
|
|
35
|
+
description: "felt safety and confidence in the person currently interacting",
|
|
36
|
+
baseline: 0.4,
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "curiosity",
|
|
40
|
+
description: "pull toward novelty, mysteries, and unanswered questions",
|
|
41
|
+
baseline: 0.5,
|
|
42
|
+
},
|
|
43
|
+
];
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { DEFAULT_EMOTIONS, type EmotionDefinition, type EmotionDelta, type EmotionSnapshot, } from "./definitions.js";
|
|
2
|
+
export { applyTemperament, type Persona, type TemperamentEntry, } from "./persona.js";
|
|
3
|
+
export { applyDeltaValue, decayValue, projectDeltas, type EmotionalState, type EmotionEntry, } from "./state.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { EmotionDefinition } from "./definitions.js";
|
|
2
|
+
/** Per-emotion personality adjustment. */
|
|
3
|
+
export interface TemperamentEntry {
|
|
4
|
+
/** Which emotion this tunes — must match a palette emotion to take effect. */
|
|
5
|
+
emotion: string;
|
|
6
|
+
/** Gain on applied deltas for this emotion (1 = neutral). */
|
|
7
|
+
sensitivity?: number;
|
|
8
|
+
/** Overrides the palette baseline for this character. */
|
|
9
|
+
baseline?: number;
|
|
10
|
+
/** How slowly/quickly this emotion fades for this character during the
|
|
11
|
+
* time-appraisal stage ("Kael's anger fades slowly" → a large value). */
|
|
12
|
+
halfLifeMinutes?: number;
|
|
13
|
+
/** A sentence the appraiser LLM reads, so judgment itself is in character
|
|
14
|
+
* ("perceived disrespect cuts deep"). */
|
|
15
|
+
disposition?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Who the agent is. Affects how events are appraised (summary + dispositions
|
|
19
|
+
* go into the prompt) and how hard feelings move (sensitivity/baseline go
|
|
20
|
+
* into the state mechanics). Reused by later pipeline stages as the agent's
|
|
21
|
+
* identity card.
|
|
22
|
+
*/
|
|
23
|
+
export interface Persona {
|
|
24
|
+
name: string;
|
|
25
|
+
/** One or two sentences of core identity, used in prompts. */
|
|
26
|
+
summary: string;
|
|
27
|
+
temperament?: TemperamentEntry[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Merge a persona's temperament into an emotion palette: baseline and
|
|
31
|
+
* sensitivity overrides are applied to matching definitions; temperament
|
|
32
|
+
* entries for emotions not in the palette are ignored (the palette decides
|
|
33
|
+
* what exists). Returns new definitions; inputs are not mutated.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyTemperament(definitions: readonly EmotionDefinition[], persona?: Persona): EmotionDefinition[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge a persona's temperament into an emotion palette: baseline and
|
|
3
|
+
* sensitivity overrides are applied to matching definitions; temperament
|
|
4
|
+
* entries for emotions not in the palette are ignored (the palette decides
|
|
5
|
+
* what exists). Returns new definitions; inputs are not mutated.
|
|
6
|
+
*/
|
|
7
|
+
export function applyTemperament(definitions, persona) {
|
|
8
|
+
const byEmotion = new Map((persona?.temperament ?? []).map((t) => [t.emotion, t]));
|
|
9
|
+
return definitions.map((def) => {
|
|
10
|
+
const t = byEmotion.get(def.name);
|
|
11
|
+
if (!t)
|
|
12
|
+
return { ...def };
|
|
13
|
+
return {
|
|
14
|
+
...def,
|
|
15
|
+
...(t.baseline !== undefined && { baseline: t.baseline }),
|
|
16
|
+
...(t.sensitivity !== undefined && { sensitivity: t.sensitivity }),
|
|
17
|
+
...(t.halfLifeMinutes !== undefined && {
|
|
18
|
+
halfLifeMinutes: t.halfLifeMinutes,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { EmotionDefinition, EmotionDelta, EmotionSnapshot } from "./definitions.js";
|
|
2
|
+
/** One slider's value plus when it last moved — what time appraisal needs. */
|
|
3
|
+
export interface EmotionEntry {
|
|
4
|
+
name: string;
|
|
5
|
+
value: number;
|
|
6
|
+
/** ISO 8601 — when this slider was last written. */
|
|
7
|
+
updatedAt: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The emotional-state boundary of the SDK. The appraisers (and later
|
|
11
|
+
* stages) touch feelings only through these five methods, so rewriting how
|
|
12
|
+
* emotional state works — different mechanics, different storage — means
|
|
13
|
+
* implementing this interface and nothing else.
|
|
14
|
+
*
|
|
15
|
+
* The shipped implementation is PostgresEmotionalState
|
|
16
|
+
* (`emocentric/postgres`): persistent, so feelings never reset between
|
|
17
|
+
* sessions or restarts.
|
|
18
|
+
*/
|
|
19
|
+
export interface EmotionalState {
|
|
20
|
+
/** The palette (post persona-temperament merge). */
|
|
21
|
+
definitions(): EmotionDefinition[];
|
|
22
|
+
/** Current value of every slider. */
|
|
23
|
+
snapshot(): Promise<EmotionSnapshot>;
|
|
24
|
+
/** Values with last-touched timestamps (the time appraiser's read). */
|
|
25
|
+
entries(): Promise<EmotionEntry[]>;
|
|
26
|
+
/** Apply deltas (sensitivity-scaled, clamped to 0..1), persist, and
|
|
27
|
+
* return the new snapshot. Deltas for unknown emotions are ignored. */
|
|
28
|
+
applyDeltas(deltas: EmotionDelta[]): Promise<EmotionSnapshot>;
|
|
29
|
+
/** Write exact values (clamped to 0..1, unknown names ignored), persist,
|
|
30
|
+
* and return the new snapshot. NOT sensitivity-scaled — used by the time
|
|
31
|
+
* appraiser (decay must land exactly on target) and for restores.
|
|
32
|
+
* `at` is the moment the write is effective AS OF (ISO 8601; default now).
|
|
33
|
+
* The time appraiser passes the event's time, so a slider's last-touched
|
|
34
|
+
* stamp records WHEN the decayed value is current — not wall-clock — and
|
|
35
|
+
* the next event measures the true elapsed gap instead of under-decaying. */
|
|
36
|
+
set(values: Partial<EmotionSnapshot>, at?: string): Promise<EmotionSnapshot>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Exponential drift toward baseline:
|
|
40
|
+
* `baseline + (current − baseline) × 0.5^(elapsed / halfLife)`.
|
|
41
|
+
* Infinity half-life = no decay; a half-life ≤ 0 snaps to baseline.
|
|
42
|
+
*/
|
|
43
|
+
export declare function decayValue(current: number, baseline: number, elapsedMs: number, halfLifeMinutes: number): number;
|
|
44
|
+
/** current + delta×sensitivity, clamped to 0..1. */
|
|
45
|
+
export declare function applyDeltaValue(current: number, delta: number, sensitivity?: number): number;
|
|
46
|
+
/**
|
|
47
|
+
* Pure projection of deltas onto a snapshot — the shared math every
|
|
48
|
+
* EmotionalState implementation (and the appraiser's dry-run mode) uses,
|
|
49
|
+
* so custom implementations don't reinvent scaling/clamping. Unknown
|
|
50
|
+
* emotions are ignored. Returns a new snapshot.
|
|
51
|
+
*/
|
|
52
|
+
export declare function projectDeltas(snapshot: EmotionSnapshot, deltas: EmotionDelta[], definitions: readonly EmotionDefinition[]): EmotionSnapshot;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { clamp01 } from "../memory/scoring.js";
|
|
2
|
+
/**
|
|
3
|
+
* Exponential drift toward baseline:
|
|
4
|
+
* `baseline + (current − baseline) × 0.5^(elapsed / halfLife)`.
|
|
5
|
+
* Infinity half-life = no decay; a half-life ≤ 0 snaps to baseline.
|
|
6
|
+
*/
|
|
7
|
+
export function decayValue(current, baseline, elapsedMs, halfLifeMinutes) {
|
|
8
|
+
if (halfLifeMinutes === Infinity)
|
|
9
|
+
return clamp01(current);
|
|
10
|
+
if (halfLifeMinutes <= 0)
|
|
11
|
+
return clamp01(baseline);
|
|
12
|
+
const factor = Math.pow(0.5, Math.max(0, elapsedMs) / (halfLifeMinutes * 60_000));
|
|
13
|
+
return clamp01(baseline + (current - baseline) * factor);
|
|
14
|
+
}
|
|
15
|
+
/** current + delta×sensitivity, clamped to 0..1. */
|
|
16
|
+
export function applyDeltaValue(current, delta, sensitivity = 1) {
|
|
17
|
+
return clamp01(current + delta * sensitivity);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pure projection of deltas onto a snapshot — the shared math every
|
|
21
|
+
* EmotionalState implementation (and the appraiser's dry-run mode) uses,
|
|
22
|
+
* so custom implementations don't reinvent scaling/clamping. Unknown
|
|
23
|
+
* emotions are ignored. Returns a new snapshot.
|
|
24
|
+
*/
|
|
25
|
+
export function projectDeltas(snapshot, deltas, definitions) {
|
|
26
|
+
const byName = new Map(definitions.map((d) => [d.name, d]));
|
|
27
|
+
const next = { ...snapshot };
|
|
28
|
+
for (const { emotion, delta } of deltas) {
|
|
29
|
+
const def = byName.get(emotion);
|
|
30
|
+
if (!def || next[emotion] === undefined)
|
|
31
|
+
continue;
|
|
32
|
+
next[emotion] = applyDeltaValue(next[emotion], delta, def.sensitivity);
|
|
33
|
+
}
|
|
34
|
+
return next;
|
|
35
|
+
}
|