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
|
@@ -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[];
|