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,50 @@
1
+ import type { Embedder } from "./store.js";
2
+ export interface OpenAIEmbedderOptions {
3
+ /** API key. Defaults to process.env.OPENAI_API_KEY. */
4
+ apiKey?: string;
5
+ /** Embedding model. Default "text-embedding-3-small". */
6
+ model?: string;
7
+ /** Output dimensions. Default 1536 (text-embedding-3-small's native size).
8
+ * text-embedding-3-* models support shrinking this (e.g. 512) for smaller,
9
+ * faster vectors at a small quality cost. MUST match the DB column. */
10
+ dimensions?: number;
11
+ /** API base URL. Default OpenAI; point it at any OpenAI-compatible
12
+ * embeddings endpoint (Together, Azure, a local proxy, …). */
13
+ baseUrl?: string;
14
+ /** Cosine-relevance floor for recall: results scoring below this on a text
15
+ * query are dropped as "no real match". Default 0.3 — tune for precision vs
16
+ * recall. (text-embedding-3 puts genuine matches well above this and
17
+ * unrelated text below ~0.25, so without a floor every query returns a full
18
+ * page of low-relevance noise.) */
19
+ minRelevance?: number;
20
+ /** Per-attempt request timeout in ms. Default 30_000. */
21
+ timeoutMs?: number;
22
+ /** Max retries on transient failures — HTTP 429, HTTP 5xx, and
23
+ * network/timeout errors. Other 4xx fail fast. Default 2 (up to 3 attempts). */
24
+ maxRetries?: number;
25
+ /** Base backoff in ms between retries; doubles each attempt with full jitter,
26
+ * capped at 8s, and overridden by a server `Retry-After` header. Default 500. */
27
+ retryBackoffMs?: number;
28
+ }
29
+ /**
30
+ * Semantic embedder backed by an OpenAI(-compatible) embeddings API. Just an
31
+ * HTTPS call — no native dependency, so it bundles cleanly everywhere (no
32
+ * webpack/Next config needed, unlike a local model). Far better recall than a
33
+ * lexical word-hashing embedder. Needs an API key; cost is tiny (text-embedding-3-small
34
+ * is ~$0.02 per million tokens). After switching an existing store to this,
35
+ * run `store.reembedAll({ confirm: true })` once.
36
+ */
37
+ export declare class OpenAIEmbedder implements Embedder {
38
+ readonly dimensions: number;
39
+ readonly minRelevance: number;
40
+ private readonly apiKey;
41
+ private readonly model;
42
+ private readonly baseUrl;
43
+ private readonly timeoutMs;
44
+ private readonly maxRetries;
45
+ private readonly retryBackoffMs;
46
+ constructor(options?: OpenAIEmbedderOptions);
47
+ embed(texts: string[]): Promise<number[][]>;
48
+ /** Exponential backoff with full jitter, capped at 8s. */
49
+ private backoffMs;
50
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Semantic embedder backed by an OpenAI(-compatible) embeddings API. Just an
3
+ * HTTPS call — no native dependency, so it bundles cleanly everywhere (no
4
+ * webpack/Next config needed, unlike a local model). Far better recall than a
5
+ * lexical word-hashing embedder. Needs an API key; cost is tiny (text-embedding-3-small
6
+ * is ~$0.02 per million tokens). After switching an existing store to this,
7
+ * run `store.reembedAll({ confirm: true })` once.
8
+ */
9
+ export class OpenAIEmbedder {
10
+ dimensions;
11
+ minRelevance;
12
+ apiKey;
13
+ model;
14
+ baseUrl;
15
+ timeoutMs;
16
+ maxRetries;
17
+ retryBackoffMs;
18
+ constructor(options = {}) {
19
+ const apiKey = options.apiKey ?? process.env.OPENAI_API_KEY;
20
+ if (!apiKey) {
21
+ throw new Error("OpenAIEmbedder needs an `apiKey` (or the OPENAI_API_KEY env var).");
22
+ }
23
+ this.apiKey = apiKey;
24
+ this.model = options.model ?? "text-embedding-3-small";
25
+ this.dimensions = options.dimensions ?? 1536;
26
+ this.minRelevance = options.minRelevance ?? 0.3;
27
+ this.baseUrl = (options.baseUrl ?? "https://api.openai.com/v1").replace(/\/$/, "");
28
+ this.timeoutMs = options.timeoutMs ?? 30_000;
29
+ this.maxRetries = options.maxRetries ?? 2;
30
+ this.retryBackoffMs = options.retryBackoffMs ?? 500;
31
+ }
32
+ async embed(texts) {
33
+ if (texts.length === 0)
34
+ return [];
35
+ const payload = JSON.stringify({
36
+ model: this.model,
37
+ input: texts,
38
+ dimensions: this.dimensions,
39
+ });
40
+ // Retry transient failures (429, 5xx, network/timeout) with jittered
41
+ // backoff; fatal failures (other 4xx) and success return immediately.
42
+ for (let attempt = 0;; attempt++) {
43
+ let response;
44
+ let transientError;
45
+ let retryAfterMs;
46
+ try {
47
+ response = await fetch(`${this.baseUrl}/embeddings`, {
48
+ method: "POST",
49
+ headers: {
50
+ "content-type": "application/json",
51
+ authorization: `Bearer ${this.apiKey}`,
52
+ },
53
+ body: payload,
54
+ signal: AbortSignal.timeout(this.timeoutMs),
55
+ });
56
+ }
57
+ catch (error) {
58
+ // Network failure or AbortSignal timeout — transient, retry.
59
+ transientError = new Error(`OpenAIEmbedder request failed (network error or >${this.timeoutMs}ms timeout): ` +
60
+ (error?.message ?? String(error)), { cause: error });
61
+ }
62
+ if (response) {
63
+ if (response.ok) {
64
+ const body = (await response.json());
65
+ const data = body.data;
66
+ if (!Array.isArray(data) || data.length !== texts.length) {
67
+ throw new Error(`OpenAIEmbedder: expected ${texts.length} embedding(s) but got ` +
68
+ `${Array.isArray(data) ? data.length : "none"} from "${this.model}".`);
69
+ }
70
+ // The API preserves input order, but sort by `index` to be safe.
71
+ const vectors = data
72
+ .slice()
73
+ .sort((a, b) => a.index - b.index)
74
+ .map((d) => d.embedding);
75
+ // Verify the returned length matches what we sized the DB column to.
76
+ // OpenRouter forwards the `dimensions` param provider-dependently, so
77
+ // some models return their native length instead — catch that here
78
+ // with a clear message rather than a cryptic pgvector insert error.
79
+ for (const vector of vectors) {
80
+ if (!Array.isArray(vector) || vector.length !== this.dimensions) {
81
+ throw new Error(`OpenAIEmbedder: model "${this.model}" returned a ` +
82
+ `${Array.isArray(vector) ? vector.length : "non-array"}-length ` +
83
+ `vector but ${this.dimensions} dimensions were requested. The ` +
84
+ "model/provider likely doesn't honor the `dimensions` parameter — " +
85
+ "set the embedder's `dimensions` to the model's native size (and " +
86
+ "reembedAll if a store already exists).");
87
+ }
88
+ }
89
+ return vectors;
90
+ }
91
+ const detail = await response.text().catch(() => "");
92
+ const error = new Error(`OpenAIEmbedder request failed (${response.status} ${response.statusText}): ${detail.slice(0, 300)}`);
93
+ // 429 (rate limit) and 5xx are transient; other 4xx won't succeed on retry.
94
+ if (response.status !== 429 && response.status < 500)
95
+ throw error;
96
+ transientError = error;
97
+ retryAfterMs = parseRetryAfter(response.headers.get("retry-after"));
98
+ }
99
+ if (attempt >= this.maxRetries)
100
+ throw transientError;
101
+ await delay(retryAfterMs ?? this.backoffMs(attempt));
102
+ }
103
+ }
104
+ /** Exponential backoff with full jitter, capped at 8s. */
105
+ backoffMs(attempt) {
106
+ const base = this.retryBackoffMs * 2 ** attempt;
107
+ return Math.random() * Math.min(base, 8_000);
108
+ }
109
+ }
110
+ function delay(ms) {
111
+ return new Promise((resolve) => setTimeout(resolve, ms));
112
+ }
113
+ /** Parse a `Retry-After` header (delta-seconds or HTTP-date) → ms, capped at 20s. */
114
+ function parseRetryAfter(value) {
115
+ if (!value)
116
+ return undefined;
117
+ const seconds = Number(value);
118
+ if (Number.isFinite(seconds))
119
+ return Math.min(Math.max(0, seconds) * 1000, 20_000);
120
+ const date = Date.parse(value);
121
+ if (!Number.isNaN(date))
122
+ return Math.min(Math.max(0, date - Date.now()), 20_000);
123
+ return undefined;
124
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Shared ranking logic so every MemoryStore implementation orders results
3
+ * the same way. Final score blends three signals:
4
+ *
5
+ * score = w.relevance * relevance (how well the fact matches the query)
6
+ * + w.importance * importance (how much the fact matters)
7
+ * + w.recency * recency (exponential decay by age)
8
+ *
9
+ * Relevance comes from the store's search (vector cosine similarity);
10
+ * importance is stored on the fact; recency is computed here with a
11
+ * configurable half-life.
12
+ */
13
+ export interface ScoringWeights {
14
+ relevance: number;
15
+ importance: number;
16
+ recency: number;
17
+ /** Age at which the recency signal halves. */
18
+ recencyHalfLifeDays: number;
19
+ }
20
+ export declare const DEFAULT_SCORING_WEIGHTS: ScoringWeights;
21
+ /** 1.0 for a brand-new fact, 0.5 after one half-life, → 0 over time. */
22
+ export declare function recencyScore(createdAt: string, halfLifeDays: number, now?: number): number;
23
+ export declare function blendScore(relevance: number, importance: number, createdAt: string, weights?: ScoringWeights, now?: number): number;
24
+ export declare function clamp01(n: number): number;
@@ -0,0 +1,20 @@
1
+ export const DEFAULT_SCORING_WEIGHTS = {
2
+ relevance: 0.65,
3
+ importance: 0.2,
4
+ recency: 0.15,
5
+ recencyHalfLifeDays: 30,
6
+ };
7
+ const MS_PER_DAY = 86_400_000;
8
+ /** 1.0 for a brand-new fact, 0.5 after one half-life, → 0 over time. */
9
+ export function recencyScore(createdAt, halfLifeDays, now = Date.now()) {
10
+ const ageMs = Math.max(0, now - Date.parse(createdAt));
11
+ return Math.pow(0.5, ageMs / (halfLifeDays * MS_PER_DAY));
12
+ }
13
+ export function blendScore(relevance, importance, createdAt, weights = DEFAULT_SCORING_WEIGHTS, now = Date.now()) {
14
+ return (weights.relevance * clamp01(relevance) +
15
+ weights.importance * clamp01(importance) +
16
+ weights.recency * recencyScore(createdAt, weights.recencyHalfLifeDays, now));
17
+ }
18
+ export function clamp01(n) {
19
+ return n < 0 ? 0 : n > 1 ? 1 : n;
20
+ }
@@ -0,0 +1,79 @@
1
+ import type { Fact, FactInput, FactKind, FactPatch } from "./fact.js";
2
+ /** A retrieval request, usually built by the recaller from the
3
+ * interpreter's description of the payload. */
4
+ export interface MemoryQuery {
5
+ /** Free-text search query. When empty, stores rank purely by
6
+ * importance + recency (useful for "what's been on my mind lately"). */
7
+ text: string;
8
+ /** Restrict to these kinds. Omit for all kinds. */
9
+ kinds?: FactKind[];
10
+ /** Restrict to facts about this subject (e.g. the current actor). */
11
+ subjectId?: string;
12
+ /** Max results. Omit to let the store's retrieval strategy decide; the
13
+ * default Postgres store returns every text match above its relevance floor
14
+ * and only applies a default cap to no-text browse queries. */
15
+ limit?: number;
16
+ /** Drop results whose blended score is below this (0..1). */
17
+ minScore?: number;
18
+ /** Drop results whose raw search relevance (cosine) is below this. Overrides
19
+ * the store and embedder defaults. Only affects text queries. */
20
+ minRelevance?: number;
21
+ /** Include facts that have been superseded. Defaults to false. */
22
+ includeSuperseded?: boolean;
23
+ }
24
+ /** A fetch result: the fact plus how it ranked. */
25
+ export interface ScoredFact {
26
+ fact: Fact;
27
+ /** Blended score (relevance + importance + recency), 0..1. */
28
+ score: number;
29
+ /** Pure search relevance before blending, 0..1. */
30
+ relevance: number;
31
+ }
32
+ /**
33
+ * The persistence boundary of the SDK — the ONLY surface the rest of the
34
+ * framework uses to touch memory. The recaller calls `fetch`, the writer
35
+ * calls `add`/`update`; nothing else reaches into storage. Rewriting the
36
+ * entire memory backend (different database, different retrieval strategy,
37
+ * a remote service) means implementing these five methods and handing the
38
+ * new store to the agent — no other code changes.
39
+ *
40
+ * Implementations must:
41
+ * - exclude superseded facts from fetch unless asked otherwise
42
+ * - return fetch results ordered by descending score
43
+ * - treat the store as scoped to one agent (multi-tenant backends take the
44
+ * agent id at construction time, not per call)
45
+ */
46
+ export interface MemoryStore {
47
+ /** Persist new facts. Generates ids/timestamps when absent and returns
48
+ * the stored facts. */
49
+ add(facts: FactInput[]): Promise<Fact[]>;
50
+ /** Search memory. This is the recaller's primitive. */
51
+ fetch(query: MemoryQuery): Promise<ScoredFact[]>;
52
+ /** Look up one fact by id, superseded or not. */
53
+ get(id: string): Promise<Fact | null>;
54
+ /** Patch a fact (the writer uses this to mark facts superseded).
55
+ * Returns the updated fact, or null if the id doesn't exist. */
56
+ update(id: string, patch: FactPatch): Promise<Fact | null>;
57
+ /** Permanently remove a fact. Returns whether it existed. */
58
+ delete(id: string): Promise<boolean>;
59
+ /** Release connections/resources, for stores that own any. */
60
+ close?(): Promise<void>;
61
+ }
62
+ /**
63
+ * Turns text into fixed-length vectors for similarity search. Swappable: the
64
+ * SDK defaults to OpenAIEmbedder (an API embedder); plug in another (e.g. a
65
+ * local model, or a different provider) by implementing these two members.
66
+ */
67
+ export interface Embedder {
68
+ /** Vector length this embedder produces — used to size DB columns. */
69
+ readonly dimensions: number;
70
+ /** Recommended cosine-relevance floor for this embedder: results scoring
71
+ * below it on a text query are treated as "no real match" and dropped. A
72
+ * lexical embedder scores 0 on no overlap (floor 0 is enough); a semantic
73
+ * embedder scores a small positive value even for unrelated text, so it
74
+ * needs a positive floor to avoid returning noise. Optional — omitted is
75
+ * treated as 0 (drop only ≤ 0). The store and per-query options override it. */
76
+ readonly minRelevance?: number;
77
+ /** Embed a batch of texts, one vector per text, same order. */
78
+ embed(texts: string[]): Promise<number[][]>;
79
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,27 @@
1
+ import type { LLMClient, LLMRequest, LLMResponse } from "../llm/types.js";
2
+ export interface OpenRouterClientOptions {
3
+ /** Model id on OpenRouter, e.g. "anthropic/claude-sonnet-4.6",
4
+ * "openai/gpt-5.2", "google/gemini-3-pro" — the developer's choice. */
5
+ model: string;
6
+ /** Defaults to process.env.OPENROUTER_API_KEY. */
7
+ apiKey?: string;
8
+ /** Default 4096. */
9
+ defaultMaxTokens?: number;
10
+ baseUrl?: string;
11
+ /** Optional OpenRouter attribution headers (shown on their dashboard). */
12
+ appUrl?: string;
13
+ appName?: string;
14
+ /** Abort requests after this many ms. Default 120_000. */
15
+ timeoutMs?: number;
16
+ }
17
+ /**
18
+ * LLMClient backed by OpenRouter's OpenAI-compatible chat completions API.
19
+ * One client = one model; run several models by creating several clients.
20
+ * Plain fetch, no SDK dependency, works in any runtime with `fetch`.
21
+ */
22
+ export declare class OpenRouterClient implements LLMClient {
23
+ private readonly options;
24
+ private readonly apiKey;
25
+ constructor(options: OpenRouterClientOptions);
26
+ complete(request: LLMRequest): Promise<LLMResponse>;
27
+ }
@@ -0,0 +1,118 @@
1
+ /**
2
+ * LLMClient backed by OpenRouter's OpenAI-compatible chat completions API.
3
+ * One client = one model; run several models by creating several clients.
4
+ * Plain fetch, no SDK dependency, works in any runtime with `fetch`.
5
+ */
6
+ export class OpenRouterClient {
7
+ options;
8
+ apiKey;
9
+ constructor(options) {
10
+ const apiKey = options.apiKey ?? process.env.OPENROUTER_API_KEY;
11
+ if (!apiKey) {
12
+ throw new Error("OpenRouterClient needs an apiKey (or OPENROUTER_API_KEY env var).");
13
+ }
14
+ this.apiKey = apiKey;
15
+ this.options = {
16
+ defaultMaxTokens: 4096,
17
+ baseUrl: "https://openrouter.ai/api/v1",
18
+ timeoutMs: 120_000,
19
+ ...options,
20
+ };
21
+ }
22
+ async complete(request) {
23
+ const messages = [];
24
+ if (request.system) {
25
+ messages.push({ role: "system", content: request.system });
26
+ }
27
+ for (const message of request.messages) {
28
+ messages.push({
29
+ role: message.role,
30
+ content: message.content.map(toOpenAIContent),
31
+ });
32
+ }
33
+ const body = {
34
+ model: this.options.model,
35
+ max_tokens: request.maxTokens ?? this.options.defaultMaxTokens,
36
+ messages,
37
+ };
38
+ if (request.jsonSchema) {
39
+ body.response_format = {
40
+ type: "json_schema",
41
+ json_schema: {
42
+ name: request.jsonSchema.name,
43
+ strict: true,
44
+ schema: request.jsonSchema.schema,
45
+ },
46
+ };
47
+ }
48
+ const headers = {
49
+ Authorization: `Bearer ${this.apiKey}`,
50
+ "Content-Type": "application/json",
51
+ };
52
+ if (this.options.appUrl)
53
+ headers["HTTP-Referer"] = this.options.appUrl;
54
+ if (this.options.appName)
55
+ headers["X-Title"] = this.options.appName;
56
+ const response = await fetch(`${this.options.baseUrl}/chat/completions`, {
57
+ method: "POST",
58
+ headers,
59
+ body: JSON.stringify(body),
60
+ signal: AbortSignal.timeout(this.options.timeoutMs),
61
+ });
62
+ const payload = (await response.json().catch(() => ({})));
63
+ if (!response.ok) {
64
+ throw new Error(`OpenRouter request failed (${response.status} ${response.statusText}): ` +
65
+ (payload.error?.message ?? "no error detail"));
66
+ }
67
+ const choice = payload.choices?.[0];
68
+ const content = choice?.message?.content;
69
+ const text = typeof content === "string"
70
+ ? content
71
+ : Array.isArray(content)
72
+ ? content
73
+ .filter((part) => part.type === "text" || part.text !== undefined)
74
+ .map((part) => part.text ?? "")
75
+ .join("")
76
+ : "";
77
+ // Empty transport output is a failure, not a "calm" response — surface
78
+ // it loudly with whatever diagnostics the provider gave us, instead of
79
+ // letting an empty string cascade into silent no-ops downstream.
80
+ if (text.trim() === "") {
81
+ const reasoningLen = typeof choice?.message?.reasoning === "string"
82
+ ? choice.message.reasoning.length
83
+ : 0;
84
+ throw new Error(`OpenRouter returned an empty completion (model ${this.options.model}, ` +
85
+ `finish_reason: ${choice?.finish_reason ?? "?"} / ${choice?.native_finish_reason ?? "?"}, ` +
86
+ `reasoning chars: ${reasoningLen}). ` +
87
+ "If finish_reason is 'length', raise maxTokens; if reasoning consumed the budget, " +
88
+ "disable/limit reasoning for this model or pick a non-reasoning model.");
89
+ }
90
+ return { text };
91
+ }
92
+ }
93
+ function toOpenAIContent(content) {
94
+ if (content.type === "text") {
95
+ return { type: "text", text: content.text };
96
+ }
97
+ return { type: "image_url", image_url: { url: mediaToUrl(content.source) } };
98
+ }
99
+ function mediaToUrl(source) {
100
+ switch (source.type) {
101
+ case "url":
102
+ return source.url;
103
+ case "base64":
104
+ return `data:${source.mimeType};base64,${source.data}`;
105
+ case "bytes":
106
+ return `data:${source.mimeType};base64,${bytesToBase64(source.data)}`;
107
+ }
108
+ }
109
+ function bytesToBase64(bytes) {
110
+ if (typeof Buffer !== "undefined") {
111
+ return Buffer.from(bytes).toString("base64");
112
+ }
113
+ let binary = "";
114
+ for (let i = 0; i < bytes.length; i += 0x8000) {
115
+ binary += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
116
+ }
117
+ return btoa(binary);
118
+ }
@@ -0,0 +1 @@
1
+ export { OpenRouterClient, type OpenRouterClientOptions } from "./client.js";
@@ -0,0 +1 @@
1
+ export { OpenRouterClient } from "./client.js";
@@ -0,0 +1,31 @@
1
+ import type { ActionExecution, ActionPlan } from "../actions/action.js";
2
+ /** Input to the act stage: the plan the processor chose. */
3
+ export interface ActInput {
4
+ plan: ActionPlan;
5
+ }
6
+ /**
7
+ * A receipt of what the agent actually did — one entry per executed action,
8
+ * carrying whatever that action returned (e.g. the message text actually
9
+ * sent). This is the record the consolidation stage reads to remember the
10
+ * agent's own actions.
11
+ */
12
+ export interface ActResult {
13
+ executions: ActionExecution[];
14
+ }
15
+ /**
16
+ * Carries out a chosen plan and records what happened. One method — swap in a
17
+ * dry-run actor (records intentions, no side effects), a rate-limited actor,
18
+ * etc., and the rest of the SDK is unchanged.
19
+ */
20
+ export interface Actor {
21
+ act(input: ActInput): Promise<ActResult>;
22
+ }
23
+ /**
24
+ * Default actor: runs the plan's actions in order (via each action's
25
+ * developer-supplied `act`), isolating per-action errors, and returns the
26
+ * executions. The SDK names no execution model — the action implementations
27
+ * the developer registered do the work and bring their own.
28
+ */
29
+ export declare class DefaultActor implements Actor {
30
+ act(input: ActInput): Promise<ActResult>;
31
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Default actor: runs the plan's actions in order (via each action's
3
+ * developer-supplied `act`), isolating per-action errors, and returns the
4
+ * executions. The SDK names no execution model — the action implementations
5
+ * the developer registered do the work and bring their own.
6
+ */
7
+ export class DefaultActor {
8
+ async act(input) {
9
+ return { executions: await input.plan.execute() };
10
+ }
11
+ }
@@ -0,0 +1,55 @@
1
+ import type { ContextEvent } from "../events/context-event.js";
2
+ import type { Fact } from "../memory/fact.js";
3
+ import type { EmotionDelta, EmotionSnapshot } from "../emotion/definitions.js";
4
+ import type { Persona } from "../emotion/persona.js";
5
+ import { type EmotionalState } from "../emotion/state.js";
6
+ import type { LLMClient } from "../llm/types.js";
7
+ export interface AppraisalInput {
8
+ event: ContextEvent;
9
+ /** The interpret/recall loop's final, memory-grounded description. */
10
+ description: string;
11
+ /** The memories that grounded it (pass `result.memories.map(m => m.fact)`). */
12
+ memories: Fact[];
13
+ state: EmotionalState;
14
+ /** Summary + dispositions go into the prompt so judgment is in character. */
15
+ persona?: Persona;
16
+ /** Default true. False = dry run: deltas + projected `after`, state untouched. */
17
+ apply?: boolean;
18
+ }
19
+ export interface AppraisalResult {
20
+ /** Raw appraised deltas (-1..+1, pre-sensitivity), unknown emotions dropped. */
21
+ deltas: EmotionDelta[];
22
+ before: EmotionSnapshot;
23
+ /** Real new state when applied; projected state on a dry run. */
24
+ after: EmotionSnapshot;
25
+ applied: boolean;
26
+ }
27
+ /**
28
+ * Decides how an event moves the agent's feelings. One method — implement
29
+ * it differently (rules, appraisal theory, a fine-tuned model) and the rest
30
+ * of the SDK keeps working.
31
+ */
32
+ export interface Appraiser {
33
+ appraise(input: AppraisalInput): Promise<AppraisalResult>;
34
+ }
35
+ export interface LLMAppraiserOptions {
36
+ llm: LLMClient;
37
+ maxTokens?: number;
38
+ }
39
+ /**
40
+ * Default appraiser: one LLM call that reads the persona (summary +
41
+ * dispositions), every emotion's description and current value, the event
42
+ * description, and the recalled memories — and returns per-emotion deltas
43
+ * with reasons.
44
+ */
45
+ export declare class LLMAppraiser implements Appraiser {
46
+ private readonly options;
47
+ constructor(options: LLMAppraiserOptions);
48
+ appraise(input: AppraisalInput): Promise<AppraisalResult>;
49
+ private systemPrompt;
50
+ private userPrompt;
51
+ }
52
+ /** Tolerant parse of appraisal output: extracts JSON from prose, drops
53
+ * deltas for emotions outside the palette, clamps deltas to [-1, 1].
54
+ * Unparseable output yields an empty delta list (a calm non-reaction). */
55
+ export declare function parseDeltas(text: string, knownEmotions: ReadonlySet<string>): EmotionDelta[];