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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +144 -0
  3. package/dist/actions/action.d.ts +70 -0
  4. package/dist/actions/action.js +47 -0
  5. package/dist/actions/index.d.ts +2 -0
  6. package/dist/actions/index.js +2 -0
  7. package/dist/actions/registry.d.ts +12 -0
  8. package/dist/actions/registry.js +26 -0
  9. package/dist/agent.d.ts +141 -0
  10. package/dist/agent.js +246 -0
  11. package/dist/blueprint/blueprint.d.ts +60 -0
  12. package/dist/blueprint/blueprint.js +20 -0
  13. package/dist/blueprint/index.d.ts +2 -0
  14. package/dist/blueprint/index.js +2 -0
  15. package/dist/blueprint/scope.d.ts +26 -0
  16. package/dist/blueprint/scope.js +21 -0
  17. package/dist/emotion/definitions.d.ts +34 -0
  18. package/dist/emotion/definitions.js +43 -0
  19. package/dist/emotion/index.d.ts +3 -0
  20. package/dist/emotion/index.js +3 -0
  21. package/dist/emotion/persona.d.ts +35 -0
  22. package/dist/emotion/persona.js +22 -0
  23. package/dist/emotion/state.d.ts +52 -0
  24. package/dist/emotion/state.js +35 -0
  25. package/dist/events/context-event.d.ts +49 -0
  26. package/dist/events/context-event.js +43 -0
  27. package/dist/events/guards.d.ts +14 -0
  28. package/dist/events/guards.js +72 -0
  29. package/dist/events/index.d.ts +8 -0
  30. package/dist/events/index.js +5 -0
  31. package/dist/events/media.d.ts +23 -0
  32. package/dist/events/media.js +16 -0
  33. package/dist/events/merge.d.ts +24 -0
  34. package/dist/events/merge.js +61 -0
  35. package/dist/events/payload.d.ts +54 -0
  36. package/dist/events/payload.js +8 -0
  37. package/dist/gateway/client.d.ts +28 -0
  38. package/dist/gateway/client.js +79 -0
  39. package/dist/gateway/index.d.ts +1 -0
  40. package/dist/gateway/index.js +1 -0
  41. package/dist/index.d.ts +9 -0
  42. package/dist/index.js +13 -0
  43. package/dist/llm/index.d.ts +1 -0
  44. package/dist/llm/index.js +1 -0
  45. package/dist/llm/types.d.ts +40 -0
  46. package/dist/llm/types.js +1 -0
  47. package/dist/memory/fact.d.ts +54 -0
  48. package/dist/memory/fact.js +1 -0
  49. package/dist/memory/index.d.ts +4 -0
  50. package/dist/memory/index.js +2 -0
  51. package/dist/memory/openai-embedder.d.ts +50 -0
  52. package/dist/memory/openai-embedder.js +124 -0
  53. package/dist/memory/scoring.d.ts +24 -0
  54. package/dist/memory/scoring.js +20 -0
  55. package/dist/memory/store.d.ts +79 -0
  56. package/dist/memory/store.js +1 -0
  57. package/dist/openrouter/client.d.ts +27 -0
  58. package/dist/openrouter/client.js +118 -0
  59. package/dist/openrouter/index.d.ts +1 -0
  60. package/dist/openrouter/index.js +1 -0
  61. package/dist/pipeline/actor.d.ts +31 -0
  62. package/dist/pipeline/actor.js +11 -0
  63. package/dist/pipeline/appraiser.d.ts +55 -0
  64. package/dist/pipeline/appraiser.js +132 -0
  65. package/dist/pipeline/consolidator.d.ts +62 -0
  66. package/dist/pipeline/consolidator.js +98 -0
  67. package/dist/pipeline/inbox.d.ts +60 -0
  68. package/dist/pipeline/inbox.js +107 -0
  69. package/dist/pipeline/index.d.ts +11 -0
  70. package/dist/pipeline/index.js +11 -0
  71. package/dist/pipeline/interpretation.d.ts +47 -0
  72. package/dist/pipeline/interpretation.js +24 -0
  73. package/dist/pipeline/llm-interpreter.d.ts +28 -0
  74. package/dist/pipeline/llm-interpreter.js +173 -0
  75. package/dist/pipeline/loop.d.ts +58 -0
  76. package/dist/pipeline/loop.js +73 -0
  77. package/dist/pipeline/processor.d.ts +63 -0
  78. package/dist/pipeline/processor.js +144 -0
  79. package/dist/pipeline/recaller.d.ts +34 -0
  80. package/dist/pipeline/recaller.js +38 -0
  81. package/dist/pipeline/time-appraiser.d.ts +85 -0
  82. package/dist/pipeline/time-appraiser.js +189 -0
  83. package/dist/pipeline/writer.d.ts +113 -0
  84. package/dist/pipeline/writer.js +210 -0
  85. package/dist/postgres/create-instance.d.ts +107 -0
  86. package/dist/postgres/create-instance.js +182 -0
  87. package/dist/postgres/emotional-state.d.ts +42 -0
  88. package/dist/postgres/emotional-state.js +134 -0
  89. package/dist/postgres/index.d.ts +5 -0
  90. package/dist/postgres/index.js +5 -0
  91. package/dist/postgres/instances.d.ts +32 -0
  92. package/dist/postgres/instances.js +77 -0
  93. package/dist/postgres/schema.d.ts +31 -0
  94. package/dist/postgres/schema.js +98 -0
  95. package/dist/postgres/short-term.d.ts +32 -0
  96. package/dist/postgres/short-term.js +95 -0
  97. package/dist/postgres/store.d.ts +85 -0
  98. package/dist/postgres/store.js +347 -0
  99. package/dist/short-term/index.d.ts +1 -0
  100. package/dist/short-term/index.js +1 -0
  101. package/dist/short-term/short-term.d.ts +66 -0
  102. package/dist/short-term/short-term.js +72 -0
  103. package/package.json +50 -0
@@ -0,0 +1,132 @@
1
+ import { projectDeltas } from "../emotion/state.js";
2
+ const DELTAS_SCHEMA = {
3
+ name: "appraisal",
4
+ schema: {
5
+ type: "object",
6
+ properties: {
7
+ deltas: {
8
+ type: "array",
9
+ items: {
10
+ type: "object",
11
+ properties: {
12
+ emotion: { type: "string" },
13
+ delta: { type: "number" },
14
+ reason: { type: "string" },
15
+ },
16
+ required: ["emotion", "delta", "reason"],
17
+ additionalProperties: false,
18
+ },
19
+ },
20
+ },
21
+ required: ["deltas"],
22
+ additionalProperties: false,
23
+ },
24
+ };
25
+ /**
26
+ * Default appraiser: one LLM call that reads the persona (summary +
27
+ * dispositions), every emotion's description and current value, the event
28
+ * description, and the recalled memories — and returns per-emotion deltas
29
+ * with reasons.
30
+ */
31
+ export class LLMAppraiser {
32
+ options;
33
+ constructor(options) {
34
+ this.options = options;
35
+ }
36
+ async appraise(input) {
37
+ const definitions = input.state.definitions();
38
+ const before = await input.state.snapshot();
39
+ const apply = input.apply ?? true;
40
+ const response = await this.options.llm.complete({
41
+ system: this.systemPrompt(input.persona),
42
+ messages: [
43
+ {
44
+ role: "user",
45
+ content: [
46
+ {
47
+ type: "text",
48
+ text: this.userPrompt(input, before),
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ jsonSchema: DELTAS_SCHEMA,
54
+ ...(this.options.maxTokens !== undefined && {
55
+ maxTokens: this.options.maxTokens,
56
+ }),
57
+ });
58
+ const known = new Set(definitions.map((d) => d.name));
59
+ const deltas = parseDeltas(response.text, known);
60
+ const after = apply
61
+ ? await input.state.applyDeltas(deltas)
62
+ : projectDeltas(before, deltas, definitions);
63
+ return { deltas, before, after, applied: apply };
64
+ }
65
+ systemPrompt(persona) {
66
+ const identity = persona
67
+ ? `You are the emotional processing of ${persona.name}: ${persona.summary}.`
68
+ : "You are the emotional processing of an agent.";
69
+ return (identity +
70
+ " Given what just happened and what the agent remembers, decide how each emotion should move.\n" +
71
+ "- Deltas are numbers from -1 to 1; most events warrant small moves (±0.05 to ±0.25). Reserve large moves for genuinely major events.\n" +
72
+ "- Only include emotions that should move; give each a short reason.\n" +
73
+ "- React in character: the dispositions describe how this agent tends to feel.\n" +
74
+ "- Memories shape feeling: the same event lands differently depending on history.\n" +
75
+ 'Respond with JSON only: {"deltas": [{"emotion": string, "delta": number, "reason": string}]}.');
76
+ }
77
+ userPrompt(input, before) {
78
+ const definitions = input.state.definitions();
79
+ const dispositions = new Map((input.persona?.temperament ?? [])
80
+ .filter((t) => t.disposition)
81
+ .map((t) => [t.emotion, t.disposition]));
82
+ const emotionLines = definitions.map((def) => {
83
+ const disposition = dispositions.get(def.name);
84
+ return (`- ${def.name} (current ${(before[def.name] ?? def.baseline).toFixed(2)}): ${def.description}` +
85
+ (disposition ? ` [disposition: ${disposition}]` : ""));
86
+ });
87
+ const memoryLines = input.memories.length === 0
88
+ ? "none relevant"
89
+ : input.memories
90
+ .map((fact, i) => `${i + 1}. [${fact.kind}] ${fact.content}`)
91
+ .join("\n");
92
+ const actor = input.event.actor.displayName ?? input.event.actor.id;
93
+ return (`EMOTIONS (value 0..1):\n${emotionLines.join("\n")}\n\n` +
94
+ `WHAT JUST HAPPENED (platform: ${input.event.platform}; actor: ${actor}; time: ${input.event.occurredAt}):\n` +
95
+ `${input.description}\n\n` +
96
+ `RELEVANT MEMORIES:\n${memoryLines}`);
97
+ }
98
+ }
99
+ /** Tolerant parse of appraisal output: extracts JSON from prose, drops
100
+ * deltas for emotions outside the palette, clamps deltas to [-1, 1].
101
+ * Unparseable output yields an empty delta list (a calm non-reaction). */
102
+ export function parseDeltas(text, knownEmotions) {
103
+ const start = text.indexOf("{");
104
+ const end = text.lastIndexOf("}");
105
+ if (start === -1 || end <= start)
106
+ return [];
107
+ try {
108
+ const parsed = JSON.parse(text.slice(start, end + 1));
109
+ if (!Array.isArray(parsed.deltas))
110
+ return [];
111
+ const deltas = [];
112
+ for (const item of parsed.deltas) {
113
+ if (typeof item !== "object" || item === null)
114
+ continue;
115
+ const { emotion, delta, reason } = item;
116
+ if (typeof emotion !== "string" || !knownEmotions.has(emotion))
117
+ continue;
118
+ const numeric = typeof delta === "number" ? delta : Number(delta);
119
+ if (!Number.isFinite(numeric))
120
+ continue;
121
+ deltas.push({
122
+ emotion,
123
+ delta: Math.max(-1, Math.min(1, numeric)),
124
+ ...(typeof reason === "string" && reason.length > 0 && { reason }),
125
+ });
126
+ }
127
+ return deltas;
128
+ }
129
+ catch {
130
+ return [];
131
+ }
132
+ }
@@ -0,0 +1,62 @@
1
+ import type { ContextEvent } from "../events/context-event.js";
2
+ import type { ActionExecution } from "../actions/action.js";
3
+ import type { Fact } from "../memory/fact.js";
4
+ import type { MemoryStore } from "../memory/store.js";
5
+ import type { Persona } from "../emotion/persona.js";
6
+ import type { LLMClient } from "../llm/types.js";
7
+ import { type WriteResult } from "./writer.js";
8
+ export interface ConsolidateInput {
9
+ event: ContextEvent;
10
+ /** What the agent actually did this turn (the act stage's executions). */
11
+ executions: ActionExecution[];
12
+ /** The interpreter's account of what prompted the agent — context only. */
13
+ description: string;
14
+ /** Memories in play, so consolidation only records NEW facts. */
15
+ memories: Fact[];
16
+ persona?: Persona;
17
+ }
18
+ /**
19
+ * Writes memory from the agent's OWN actions. Where the writer records what
20
+ * happened TO the agent, the consolidator records what the agent DID — so a
21
+ * sent message like "great seeing you, I met Bill" leaves behind that the
22
+ * agent met Bill (and Bill becomes someone it knows). One method — swap it.
23
+ */
24
+ export interface Consolidator {
25
+ consolidate(input: ConsolidateInput): Promise<WriteResult>;
26
+ }
27
+ export interface LLMConsolidatorOptions {
28
+ llm: LLMClient;
29
+ store: MemoryStore;
30
+ /** Cap on facts extracted per turn. Default 5. */
31
+ maxFacts?: number;
32
+ /** Similarity at/above which a candidate is a known duplicate. Default 0.85. */
33
+ duplicateThreshold?: number;
34
+ /** Similarity at/above which a same-kind/same-subject fact is superseded.
35
+ * Default 0.5. */
36
+ supersedeThreshold?: number;
37
+ maxTokens?: number;
38
+ }
39
+ /**
40
+ * Default consolidator. After the agent acts, it reads what the agent said
41
+ * and did and extracts durable facts about the agent and the people it named
42
+ * or interacted with — reusing the writer's extraction discipline and
43
+ * dedupe/supersede reconciliation, but from the agent's perspective.
44
+ */
45
+ export declare class LLMConsolidator implements Consolidator {
46
+ private readonly llm;
47
+ private readonly store;
48
+ private readonly maxFacts;
49
+ private readonly duplicateThreshold;
50
+ private readonly supersedeThreshold;
51
+ private readonly maxTokens;
52
+ constructor(options: LLMConsolidatorOptions);
53
+ consolidate(input: ConsolidateInput): Promise<WriteResult>;
54
+ private systemPrompt;
55
+ private userPrompt;
56
+ }
57
+ /** Render the agent's content-bearing actions — those that produced text
58
+ * output (a sent message, a written note) — into lines the LLM can extract
59
+ * facts from. Param-only or empty actions (silence, a bare reaction) carry
60
+ * nothing durable, so they're skipped; "" means the agent produced no content
61
+ * this turn and consolidation can be skipped entirely. */
62
+ export declare function summarizeExecutions(executions: ActionExecution[]): string;
@@ -0,0 +1,98 @@
1
+ import { WRITE_SCHEMA, parseCandidates, reconcile, } from "./writer.js";
2
+ const EMPTY = () => ({ added: [], superseded: [], skipped: [] });
3
+ /**
4
+ * Default consolidator. After the agent acts, it reads what the agent said
5
+ * and did and extracts durable facts about the agent and the people it named
6
+ * or interacted with — reusing the writer's extraction discipline and
7
+ * dedupe/supersede reconciliation, but from the agent's perspective.
8
+ */
9
+ export class LLMConsolidator {
10
+ llm;
11
+ store;
12
+ maxFacts;
13
+ duplicateThreshold;
14
+ supersedeThreshold;
15
+ maxTokens;
16
+ constructor(options) {
17
+ this.llm = options.llm;
18
+ this.store = options.store;
19
+ this.maxFacts = options.maxFacts ?? 5;
20
+ this.duplicateThreshold = options.duplicateThreshold ?? 0.85;
21
+ this.supersedeThreshold = options.supersedeThreshold ?? 0.5;
22
+ this.maxTokens = options.maxTokens;
23
+ }
24
+ async consolidate(input) {
25
+ const did = summarizeExecutions(input.executions);
26
+ // Nothing with substance was done (silence, or side-effect-only actions
27
+ // with no content) — skip the LLM call entirely.
28
+ if (did === "")
29
+ return EMPTY();
30
+ const response = await this.llm.complete({
31
+ system: this.systemPrompt(input.persona),
32
+ messages: [
33
+ { role: "user", content: [{ type: "text", text: this.userPrompt(input, did) }] },
34
+ ],
35
+ jsonSchema: WRITE_SCHEMA,
36
+ ...(this.maxTokens !== undefined && { maxTokens: this.maxTokens }),
37
+ });
38
+ const candidates = parseCandidates(response.text, input.memories, this.maxFacts);
39
+ const result = EMPTY();
40
+ for (const candidate of candidates) {
41
+ await reconcile(candidate, {
42
+ store: this.store,
43
+ duplicateThreshold: this.duplicateThreshold,
44
+ supersedeThreshold: this.supersedeThreshold,
45
+ sourceEventId: input.event.id,
46
+ shownMemories: input.memories,
47
+ extraMetadata: { origin: "consolidation" },
48
+ }, result);
49
+ }
50
+ return result;
51
+ }
52
+ systemPrompt(persona) {
53
+ const who = persona ? `${persona.name}` : "the agent";
54
+ const identity = persona
55
+ ? `You are the memory-consolidation system of ${persona.name}: ${persona.summary}.`
56
+ : "You are the memory-consolidation system of an agent.";
57
+ return (identity +
58
+ ` The agent just acted. Below is what ${who} said and did. Extract the durable facts ${who} should remember from its OWN actions — about itself and about the people it named or interacted with.\n` +
59
+ "RULES:\n" +
60
+ `- EXTRACT from what ${who} did, don't transcribe it: store the underlying fact. "${who}: great seeing you, I met Bill last night" leaves behind that ${who} met someone named Bill — and that Bill is now someone ${who} knows.\n` +
61
+ '- ATOMIC: each fact is exactly ONE piece of information; split compound statements.\n' +
62
+ `- ESTABLISH PEOPLE: when the agent meets or names someone new, record them as a full fact (kind "user") so the relationship persists — e.g. "Bill is someone ${who} met at the gym." NEVER store a bare name like "Bill" or any fragment; every fact is a complete sentence.\n` +
63
+ `- DURABLE ONLY: keep names, identities, relationships, commitments ${who} made, plans, and significant dated events. DROP the act mechanics ("sent a message", "replied"), pleasantries, and ${who}'s feelings or mood.\n` +
64
+ "- NEW ONLY: record nothing already covered by KNOWN MEMORIES. If something corrects a known memory, output the corrected fact and set `replaces` to that memory's number.\n" +
65
+ `- STANDALONE: each fact must make sense years from now — real names (never "he"/"they"), absolute dates (the time is given), and name ${who} explicitly rather than saying "the agent" or "I".\n` +
66
+ '- kind: "user" (a person the agent now knows something about, incl. someone it just met), "event" (a real-world happening the agent did or referenced — never "a message was sent"), "self" (RARE: a concrete durable thing about the agent it revealed), "persona" (rare).\n' +
67
+ "- importance 0..1: trivia ~0.2, ordinary ~0.4-0.6, identity-defining or relationship-forming 0.7-0.9.\n" +
68
+ "- Prefer a FEW high-quality facts. An empty list is correct when the agent's actions revealed nothing durable.\n" +
69
+ 'Respond with JSON only: {"facts": [{"kind": string, "content": string, "subject": string|null, "importance": number, "replaces": integer|null}]}.');
70
+ }
71
+ userPrompt(input, did) {
72
+ const known = input.memories.length === 0
73
+ ? "none"
74
+ : input.memories
75
+ .map((fact, i) => `${i + 1}. [${fact.kind}] ${fact.content}`)
76
+ .join("\n");
77
+ return (`TIME: ${input.event.occurredAt}\n\n` +
78
+ `CONTEXT (what prompted the agent — for grounding only, do not extract facts from this):\n${input.description}\n\n` +
79
+ `WHAT THE AGENT DID (extract durable facts from this):\n${did}\n\n` +
80
+ `KNOWN MEMORIES:\n${known}`);
81
+ }
82
+ }
83
+ /** Render the agent's content-bearing actions — those that produced text
84
+ * output (a sent message, a written note) — into lines the LLM can extract
85
+ * facts from. Param-only or empty actions (silence, a bare reaction) carry
86
+ * nothing durable, so they're skipped; "" means the agent produced no content
87
+ * this turn and consolidation can be skipped entirely. */
88
+ export function summarizeExecutions(executions) {
89
+ const lines = [];
90
+ for (const execution of executions) {
91
+ if (execution.status !== "succeeded")
92
+ continue;
93
+ if (typeof execution.result === "string" && execution.result.trim() !== "") {
94
+ lines.push(`- ${execution.name}: "${execution.result.trim()}"`);
95
+ }
96
+ }
97
+ return lines.join("\n");
98
+ }
@@ -0,0 +1,60 @@
1
+ import type { ContextEvent } from "../events/context-event.js";
2
+ /**
3
+ * Returned by `enqueue` for an event that was folded into a later, merged
4
+ * heartbeat instead of producing its own result. The real result belongs to
5
+ * the merged event whose id is `into`; this caller should do nothing.
6
+ */
7
+ export interface MergedOutcome {
8
+ merged: true;
9
+ /** Id of the merged event whose result covers this one. */
10
+ into: string;
11
+ }
12
+ /** What `enqueue` resolves to: the heartbeat result, or a merged marker. */
13
+ export type EnqueueOutcome<R> = R | MergedOutcome;
14
+ /** Narrow an enqueue outcome to the merged-marker case. */
15
+ export declare function isMerged<R>(outcome: EnqueueOutcome<R>): outcome is MergedOutcome;
16
+ /**
17
+ * Thrown by the runner at a cancellation checkpoint when its signal has
18
+ * aborted. The inbox catches it to mean "a new event arrived in time to
19
+ * merge — re-run the heartbeat with the bigger batch"; it is never a real
20
+ * failure.
21
+ */
22
+ export declare class HeartbeatCancelled extends Error {
23
+ constructor();
24
+ }
25
+ export interface RunControls {
26
+ /** Aborts when a mergeable event arrives before the commit point; the
27
+ * runner must check it at its stage boundaries and throw HeartbeatCancelled. */
28
+ signal: AbortSignal;
29
+ /** Call synchronously the instant the heartbeat passes its last cancellable
30
+ * point (feelings about to persist). After this the inbox stops cancelling
31
+ * and routes new events to the next round. MUST be called with no `await`
32
+ * between the final checkpoint and this call, or the cancel/commit race is
33
+ * not airtight. */
34
+ onCommit(): void;
35
+ }
36
+ /** Runs one (already-merged) heartbeat under cancellation control. */
37
+ export type HeartbeatRunner<R> = (event: ContextEvent, controls: RunControls) => Promise<R>;
38
+ /**
39
+ * Per-instance event funnel. Serializes heartbeats (one at a time, killing
40
+ * the lost-feeling race that concurrent runs cause) and coalesces bursts: an
41
+ * event that arrives while the current heartbeat is still cancellable (before
42
+ * its feelings persist) is merged into it and the heartbeat re-runs as one.
43
+ * Once a heartbeat commits, later events wait for the next round.
44
+ *
45
+ * Generic over the result type so it carries no dependency on the Agent.
46
+ */
47
+ export declare class InstanceInbox<R> {
48
+ private readonly run;
49
+ /** Waiters of the in-flight round (null when idle). The same array the
50
+ * running heartbeat reads, so a merged-in event is picked up on re-run. */
51
+ private round;
52
+ /** Waiters that arrived after the current round committed. */
53
+ private queued;
54
+ /** Has the in-flight round passed its last cancellable point. */
55
+ private committed;
56
+ private controller;
57
+ constructor(run: HeartbeatRunner<R>);
58
+ enqueue(event: ContextEvent): Promise<EnqueueOutcome<R>>;
59
+ private runRound;
60
+ }
@@ -0,0 +1,107 @@
1
+ import { mergeEvents } from "../events/merge.js";
2
+ /** Narrow an enqueue outcome to the merged-marker case. */
3
+ export function isMerged(outcome) {
4
+ return (typeof outcome === "object" &&
5
+ outcome !== null &&
6
+ outcome.merged === true);
7
+ }
8
+ /**
9
+ * Thrown by the runner at a cancellation checkpoint when its signal has
10
+ * aborted. The inbox catches it to mean "a new event arrived in time to
11
+ * merge — re-run the heartbeat with the bigger batch"; it is never a real
12
+ * failure.
13
+ */
14
+ export class HeartbeatCancelled extends Error {
15
+ constructor() {
16
+ super("heartbeat cancelled for merge");
17
+ this.name = "HeartbeatCancelled";
18
+ }
19
+ }
20
+ /**
21
+ * Per-instance event funnel. Serializes heartbeats (one at a time, killing
22
+ * the lost-feeling race that concurrent runs cause) and coalesces bursts: an
23
+ * event that arrives while the current heartbeat is still cancellable (before
24
+ * its feelings persist) is merged into it and the heartbeat re-runs as one.
25
+ * Once a heartbeat commits, later events wait for the next round.
26
+ *
27
+ * Generic over the result type so it carries no dependency on the Agent.
28
+ */
29
+ export class InstanceInbox {
30
+ run;
31
+ /** Waiters of the in-flight round (null when idle). The same array the
32
+ * running heartbeat reads, so a merged-in event is picked up on re-run. */
33
+ round = null;
34
+ /** Waiters that arrived after the current round committed. */
35
+ queued = [];
36
+ /** Has the in-flight round passed its last cancellable point. */
37
+ committed = false;
38
+ controller = null;
39
+ constructor(run) {
40
+ this.run = run;
41
+ }
42
+ enqueue(event) {
43
+ return new Promise((resolve, reject) => {
44
+ const waiter = { event, resolve, reject };
45
+ if (this.round === null) {
46
+ this.round = [waiter];
47
+ void this.runRound();
48
+ }
49
+ else if (!this.committed) {
50
+ // Still cancellable: fold in and restart as one merged heartbeat.
51
+ this.round.push(waiter);
52
+ this.controller?.abort();
53
+ }
54
+ else {
55
+ // Already committed its feelings — this belongs to the next round.
56
+ this.queued.push(waiter);
57
+ }
58
+ });
59
+ }
60
+ async runRound() {
61
+ for (;;) {
62
+ this.committed = false;
63
+ this.controller = new AbortController();
64
+ const waiters = this.round;
65
+ const merged = mergeEvents(waiters.map((w) => w.event));
66
+ try {
67
+ const result = await this.run(merged, {
68
+ signal: this.controller.signal,
69
+ onCommit: () => {
70
+ this.committed = true;
71
+ },
72
+ });
73
+ settleSuccess(waiters, merged.id, result);
74
+ break;
75
+ }
76
+ catch (error) {
77
+ // A merge folded a new waiter in and aborted us: re-run, bigger batch.
78
+ if (error instanceof HeartbeatCancelled)
79
+ continue;
80
+ settleFailure(waiters, error);
81
+ break;
82
+ }
83
+ }
84
+ this.round = null;
85
+ this.controller = null;
86
+ // Drain anything that queued behind the committed round.
87
+ if (this.queued.length > 0) {
88
+ this.round = this.queued;
89
+ this.queued = [];
90
+ void this.runRound();
91
+ }
92
+ }
93
+ }
94
+ function settleSuccess(waiters, mergedId, result) {
95
+ // The most recent event owns the real result — the merged event carries
96
+ // its reply handle. Earlier events learn they were absorbed.
97
+ waiters.forEach((waiter, i) => {
98
+ if (i === waiters.length - 1)
99
+ waiter.resolve(result);
100
+ else
101
+ waiter.resolve({ merged: true, into: mergedId });
102
+ });
103
+ }
104
+ function settleFailure(waiters, error) {
105
+ for (const waiter of waiters)
106
+ waiter.reject(error);
107
+ }
@@ -0,0 +1,11 @@
1
+ export { InterpreterRouter, type Interpretation, type Interpreter, } from "./interpretation.js";
2
+ export { LLMInterpreter, parseInterpretation, type LLMInterpreterOptions, } from "./llm-interpreter.js";
3
+ export { MemoryRecaller, type MemoryRecallerOptions, type Recaller, } from "./recaller.js";
4
+ export { InterpretRecallLoop, type InterpretRecallLoopOptions, type InterpretRecallResult, type StopReason, } from "./loop.js";
5
+ export { LLMAppraiser, parseDeltas, type AppraisalInput, type AppraisalResult, type Appraiser, type LLMAppraiserOptions, } from "./appraiser.js";
6
+ export { DecayTimeAppraiser, LLMTimeAppraiser, type DecayTimeAppraiserOptions, type LLMTimeAppraiserOptions, type TimeAppraisalInput, type TimeAppraisalResult, type TimeAppraiser, } from "./time-appraiser.js";
7
+ export { LLMWriter, parseCandidates, type FactCandidate, type LLMWriterOptions, type WriteInput, type WriteResult, type Writer, } from "./writer.js";
8
+ export { LLMProcessor, parseSelections, type LLMProcessorOptions, type ProcessInput, type ProcessResult, type Processor, } from "./processor.js";
9
+ export { DefaultActor, type ActInput, type ActResult, type Actor as PlanActor, } from "./actor.js";
10
+ export { LLMConsolidator, summarizeExecutions, type Consolidator, type ConsolidateInput, type LLMConsolidatorOptions, } from "./consolidator.js";
11
+ export { InstanceInbox, HeartbeatCancelled, isMerged, type EnqueueOutcome, type MergedOutcome, type HeartbeatRunner, type RunControls, } from "./inbox.js";
@@ -0,0 +1,11 @@
1
+ export { InterpreterRouter, } from "./interpretation.js";
2
+ export { LLMInterpreter, parseInterpretation, } from "./llm-interpreter.js";
3
+ export { MemoryRecaller, } from "./recaller.js";
4
+ export { InterpretRecallLoop, } from "./loop.js";
5
+ export { LLMAppraiser, parseDeltas, } from "./appraiser.js";
6
+ export { DecayTimeAppraiser, LLMTimeAppraiser, } from "./time-appraiser.js";
7
+ export { LLMWriter, parseCandidates, } from "./writer.js";
8
+ export { LLMProcessor, parseSelections, } from "./processor.js";
9
+ export { DefaultActor, } from "./actor.js";
10
+ export { LLMConsolidator, summarizeExecutions, } from "./consolidator.js";
11
+ export { InstanceInbox, HeartbeatCancelled, isMerged, } from "./inbox.js";
@@ -0,0 +1,47 @@
1
+ import type { ContextEvent } from "../events/context-event.js";
2
+ import type { PayloadKind } from "../events/payload.js";
3
+ import type { Fact } from "../memory/fact.js";
4
+ import type { Engagement } from "../short-term/short-term.js";
5
+ /** What an interpreter concluded about an event. */
6
+ export interface Interpretation {
7
+ /** Rich, factual text description of what happened, grounded in the
8
+ * provided memories where they helped identify things. */
9
+ description: string;
10
+ /** The interpreter's self-report: is every notable element of the event
11
+ * explained by the event itself or a memory? */
12
+ fullyInterpreted: boolean;
13
+ /** Up to ~3 short search queries for what additional memory would help
14
+ * ("who is the woman in the photo"). Feed the recaller. */
15
+ gaps: string[];
16
+ }
17
+ /**
18
+ * Turns an event (plus whatever memories are in hand) into a description.
19
+ * The SDK ships LLMInterpreter (multimodal, handles every payload kind);
20
+ * specialized interpreters — e.g. a transcription-backed audio interpreter,
21
+ * a game-state interpreter — implement this same interface and are wired in
22
+ * per payload kind via {@link InterpreterRouter}.
23
+ */
24
+ export interface Interpreter {
25
+ /**
26
+ * @param recent The agent's short-term window — recent engagements with this
27
+ * instance, for understanding replies and follow-ups. Omitted/empty on the
28
+ * neutral first pass of the loop.
29
+ */
30
+ interpret(event: ContextEvent, memories: Fact[], recent?: Engagement[]): Promise<Interpretation>;
31
+ }
32
+ /**
33
+ * Dispatches to a different Interpreter per payload kind, falling back to a
34
+ * default for unregistered kinds. This is how media types get split across
35
+ * specialized interpreters without the loop knowing or caring:
36
+ *
37
+ * const interpreter = new InterpreterRouter(llmInterpreter)
38
+ * .register("audio", new WhisperAudioInterpreter(...))
39
+ * .register("video", new VideoFrameInterpreter(...));
40
+ */
41
+ export declare class InterpreterRouter implements Interpreter {
42
+ private readonly fallback;
43
+ private readonly byKind;
44
+ constructor(fallback: Interpreter);
45
+ register(kind: PayloadKind, interpreter: Interpreter): this;
46
+ interpret(event: ContextEvent, memories: Fact[], recent?: Engagement[]): Promise<Interpretation>;
47
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Dispatches to a different Interpreter per payload kind, falling back to a
3
+ * default for unregistered kinds. This is how media types get split across
4
+ * specialized interpreters without the loop knowing or caring:
5
+ *
6
+ * const interpreter = new InterpreterRouter(llmInterpreter)
7
+ * .register("audio", new WhisperAudioInterpreter(...))
8
+ * .register("video", new VideoFrameInterpreter(...));
9
+ */
10
+ export class InterpreterRouter {
11
+ fallback;
12
+ byKind = new Map();
13
+ constructor(fallback) {
14
+ this.fallback = fallback;
15
+ }
16
+ register(kind, interpreter) {
17
+ this.byKind.set(kind, interpreter);
18
+ return this;
19
+ }
20
+ interpret(event, memories, recent) {
21
+ const interpreter = this.byKind.get(event.payload.kind) ?? this.fallback;
22
+ return interpreter.interpret(event, memories, recent);
23
+ }
24
+ }
@@ -0,0 +1,28 @@
1
+ import type { ContextEvent } from "../events/context-event.js";
2
+ import type { Fact } from "../memory/fact.js";
3
+ import type { LLMClient } from "../llm/types.js";
4
+ import { type Engagement } from "../short-term/short-term.js";
5
+ import type { Interpretation, Interpreter } from "./interpretation.js";
6
+ export interface LLMInterpreterOptions {
7
+ llm: LLMClient;
8
+ /** Optional one-line description of the agent, to anchor perspective
9
+ * ("You perceive on behalf of Lyra, a cellist..."). */
10
+ personaHint?: string;
11
+ maxTokens?: number;
12
+ /** Cap on `gaps` returned per interpretation. Default 3. */
13
+ maxGaps?: number;
14
+ }
15
+ /**
16
+ * The default multimodal interpreter: one LLM call that sees the event's
17
+ * text/images directly (audio/video via their transcripts or captions) plus
18
+ * the memories recalled so far, and produces a grounded description.
19
+ */
20
+ export declare class LLMInterpreter implements Interpreter {
21
+ private readonly options;
22
+ constructor(options: LLMInterpreterOptions);
23
+ interpret(event: ContextEvent, memories: Fact[], recent?: Engagement[]): Promise<Interpretation>;
24
+ private systemPrompt;
25
+ }
26
+ /** Tolerant parse: providers without strict schema support may wrap the
27
+ * JSON in prose or fences. Worst case, the raw text becomes the description. */
28
+ export declare function parseInterpretation(text: string, maxGaps: number): Interpretation;