@ay-2814/era-sdk 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/README.md ADDED
@@ -0,0 +1,53 @@
1
+ # @season/era-sdk
2
+
3
+ Drop-in cognitive context for any conversational AI agent. Zero runtime
4
+ dependencies (uses global `fetch`).
5
+
6
+ ```bash
7
+ npm install @season/era-sdk
8
+ ```
9
+
10
+ ## Quick start
11
+
12
+ ```ts
13
+ import { Era } from "@season/era-sdk";
14
+
15
+ const era = new Era({
16
+ apiKey: process.env.ERA_API_KEY!,
17
+ baseUrl: process.env.ERA_URL!,
18
+ partnerId: "your_partner_id",
19
+ });
20
+
21
+ const block = await era.turnContext(messages, { userId, sessionId });
22
+ systemPrompt += "\n\n" + block;
23
+ ```
24
+
25
+ For Vercel AI SDK auto-injection middleware, import `@season/era-sdk/ai-sdk`.
26
+
27
+ ## Modes & production behavior
28
+
29
+ - `mode: "async"` (default) — stale-while-revalidate; instant response with the
30
+ previous turn's block. `mode: "sync"` waits for the current turn.
31
+ - **Fail-open**: any error returns `""` — your agent never breaks.
32
+ - **Bounded retries** on transient failures (timeout/network/5xx); `4xx` is
33
+ terminal. Configure with `maxRetries` (default 1).
34
+ - **Timeouts**: 2s async / 30s sync, override with `timeoutMs`.
35
+ - **Logging**: pass a `logger` (`{ warn, info }`) to integrate with pino/winston,
36
+ or `debug: true` for verbose per-turn console output. Warnings only by default.
37
+
38
+ ## Options
39
+
40
+ | Option | Default | Notes |
41
+ |--------|---------|-------|
42
+ | `mode` | `"async"` | `"async"` \| `"sync"` |
43
+ | `brandVoice` | — | words the `[REC]` action |
44
+ | `timeoutMs` | 2000 / 30000 | per request |
45
+ | `maxRetries` | 1 | transient-only |
46
+ | `logger` / `debug` | console / false | observability |
47
+
48
+ ## Develop
49
+
50
+ ```bash
51
+ npm run build # tsc -> dist/
52
+ npm test # node --test
53
+ ```
@@ -0,0 +1,4 @@
1
+ import type { LanguageModel, LanguageModelMiddleware } from "ai";
2
+ import type { Era, TurnContextOptions } from "./index.js";
3
+ export declare function eraMiddleware(era: Era, options?: TurnContextOptions): LanguageModelMiddleware;
4
+ export declare function withEra(model: LanguageModel, era: Era, options?: TurnContextOptions): LanguageModel;
package/dist/ai-sdk.js ADDED
@@ -0,0 +1,55 @@
1
+ // @season/era-sdk/ai-sdk — Vercel AI SDK middleware. Auto-injects Era's
2
+ // cognitive block into the system prompt on every model call.
3
+ //
4
+ // The whole integration:
5
+ //
6
+ // import { Era } from "@season/era-sdk";
7
+ // import { withEra } from "@season/era-sdk/ai-sdk";
8
+ //
9
+ // const era = new Era({ apiKey, baseUrl, partnerId });
10
+ // const model = withEra(anthropic("claude-sonnet-4-5"), era, { userId, sessionId });
11
+ //
12
+ // // streamText/generateText through `model` now carry Era context automatically.
13
+ import { wrapLanguageModel } from "ai";
14
+ export function eraMiddleware(era, options = {}) {
15
+ // The middleware runs once per model invocation (steps, retries); log the
16
+ // final prompt only on the first injection so a single request prints once.
17
+ let loggedPrompt = false;
18
+ return {
19
+ specificationVersion: "v3",
20
+ transformParams: async ({ params }) => {
21
+ const prompt = params.prompt;
22
+ if (!Array.isArray(prompt))
23
+ return params;
24
+ const conversation = prompt.filter((m) => m.role === "user" || m.role === "assistant");
25
+ const block = await era.turnContext(conversation, options);
26
+ if (!block)
27
+ return params;
28
+ const newPrompt = [...prompt];
29
+ const sysIdx = newPrompt.findIndex((m) => m.role === "system");
30
+ if (sysIdx >= 0 && typeof newPrompt[sysIdx].content === "string") {
31
+ newPrompt[sysIdx] = {
32
+ ...newPrompt[sysIdx],
33
+ content: `${newPrompt[sysIdx].content}\n\n${block}`,
34
+ };
35
+ }
36
+ else {
37
+ newPrompt.unshift({ role: "system", content: block });
38
+ }
39
+ if (!loggedPrompt) {
40
+ loggedPrompt = true;
41
+ const sys = newPrompt.find((m) => m.role === "system");
42
+ const sysContent = typeof sys?.content === "string" ? sys.content : "";
43
+ console.log(`[era-sdk] system prompt sent to model (${sysContent.length} chars):\n--- BEGIN SYSTEM PROMPT ---\n${sysContent}\n--- END SYSTEM PROMPT ---`);
44
+ }
45
+ return { ...params, prompt: newPrompt };
46
+ },
47
+ };
48
+ }
49
+ export function withEra(model, era, options = {}) {
50
+ return wrapLanguageModel({
51
+ // biome-ignore lint/suspicious/noExplicitAny: wrapLanguageModel accepts provider model objects
52
+ model: model,
53
+ middleware: eraMiddleware(era, options),
54
+ });
55
+ }
@@ -0,0 +1,55 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export type EraMode = "sync" | "async";
3
+ /** Minimal logger surface. Defaults to console; pass your own to integrate
4
+ * with pino/winston, or `{ warn: () => {}, info: () => {} }` to silence. */
5
+ export interface EraLogger {
6
+ warn: (msg: string) => void;
7
+ info: (msg: string) => void;
8
+ }
9
+ export interface EraOptions {
10
+ apiKey: string;
11
+ baseUrl: string;
12
+ partnerId: string;
13
+ userId?: string;
14
+ sessionId?: string;
15
+ /** "async" (default): instant response with the previous turn's block.
16
+ * "sync": wait for the current turn's block — adds pipeline latency to every call. */
17
+ mode?: EraMode;
18
+ /** Partner brand voice — the phrasing layer words the [REC] action with it. */
19
+ brandVoice?: string;
20
+ /** Override the request timeout (ms). Defaults: 2000 async, 30000 sync. */
21
+ timeoutMs?: number;
22
+ /** Retries on transient failures (timeout/network/5xx). 4xx never retries. Default 1. */
23
+ maxRetries?: number;
24
+ /** Custom logger. Defaults to console.warn for warnings; info is silent unless `debug`. */
25
+ logger?: EraLogger;
26
+ /** When true (and no custom logger), per-turn info is logged to console. Default false. */
27
+ debug?: boolean;
28
+ }
29
+ export interface EraMessage {
30
+ role: string;
31
+ content: unknown;
32
+ }
33
+ export interface EraTurn {
34
+ role: "user" | "assistant";
35
+ turn_index: number;
36
+ text: string;
37
+ }
38
+ export interface TurnContextOptions {
39
+ userId?: string;
40
+ sessionId?: string;
41
+ }
42
+ export declare function messagesToTurns(messages: EraMessage[]): EraTurn[];
43
+ export declare class Era {
44
+ private readonly opts;
45
+ private readonly log;
46
+ private lastTurn;
47
+ constructor(opts: EraOptions);
48
+ /**
49
+ * Get the cognitive block for the current turn. Returns "" on any failure
50
+ * (fail-open: your agent must never break because Era is unreachable).
51
+ * The last message must be from the user.
52
+ */
53
+ turnContext(messages: EraMessage[], options?: TurnContextOptions): Promise<string>;
54
+ private fetchTurn;
55
+ }
package/dist/index.js ADDED
@@ -0,0 +1,150 @@
1
+ // @season/era-sdk — provider-agnostic core. Zero runtime dependencies (global fetch).
2
+ //
3
+ // Usage with ANY framework:
4
+ //
5
+ // import { Era } from "@season/era-sdk";
6
+ //
7
+ // const era = new Era({
8
+ // apiKey: process.env.ERA_API_KEY!,
9
+ // baseUrl: process.env.ERA_URL!,
10
+ // partnerId: "your_partner_id",
11
+ // });
12
+ //
13
+ // const block = await era.turnContext(messages, { userId, sessionId });
14
+ // systemPrompt += "\n\n" + block; // inject wherever your stack puts system context
15
+ //
16
+ // For Vercel AI SDK auto-injection, see "@season/era-sdk/ai-sdk".
17
+ export const VERSION = "0.1.0";
18
+ const ERA_ASYNC_TIMEOUT_MS = 2000;
19
+ // Sync mode waits for the full extraction pipeline (two LLM calls) before
20
+ // responding, so it needs far more headroom than the async fast path.
21
+ const ERA_SYNC_TIMEOUT_MS = 30000;
22
+ const DEFAULT_MAX_RETRIES = 1;
23
+ const RETRY_BACKOFF_BASE_MS = 100;
24
+ function extractText(content) {
25
+ if (typeof content === "string")
26
+ return content;
27
+ if (Array.isArray(content)) {
28
+ return content
29
+ .filter((p) => typeof p === "object" && p !== null && p.type === "text")
30
+ .map((p) => p.text)
31
+ .join(" ");
32
+ }
33
+ return "";
34
+ }
35
+ export function messagesToTurns(messages) {
36
+ const turns = [];
37
+ let idx = 0;
38
+ for (const m of messages) {
39
+ if (m.role !== "user" && m.role !== "assistant")
40
+ continue;
41
+ const text = extractText(m.content);
42
+ if (!text.trim())
43
+ continue;
44
+ idx += 1;
45
+ turns.push({ role: m.role, turn_index: idx, text });
46
+ }
47
+ return turns;
48
+ }
49
+ async function defaultSessionId(turns) {
50
+ const firstUser = turns.find((t) => t.role === "user")?.text ?? "";
51
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(firstUser));
52
+ const hex = Array.from(new Uint8Array(digest))
53
+ .map((b) => b.toString(16).padStart(2, "0"))
54
+ .join("");
55
+ return `sdk_${hex.slice(0, 16)}`;
56
+ }
57
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
58
+ export class Era {
59
+ opts;
60
+ log;
61
+ // Memoizes the latest turn lookup. streamText invokes the middleware once
62
+ // per model call (steps, retries), so without this a single chat request
63
+ // pays one Era round-trip per invocation. Keyed by user/session/turn so a
64
+ // shared instance never serves another conversation's block.
65
+ lastTurn = null;
66
+ constructor(opts) {
67
+ const mode = opts.mode ?? "async";
68
+ if (mode !== "sync" && mode !== "async") {
69
+ throw new Error(`era-sdk: mode must be "sync" or "async", got "${mode}"`);
70
+ }
71
+ this.opts = { ...opts, baseUrl: opts.baseUrl.replace(/\/+$/, "") };
72
+ this.log =
73
+ opts.logger ?? {
74
+ warn: (m) => console.warn(`[era-sdk] ${m}`),
75
+ info: (m) => {
76
+ if (opts.debug)
77
+ console.log(`[era-sdk] ${m}`);
78
+ },
79
+ };
80
+ }
81
+ /**
82
+ * Get the cognitive block for the current turn. Returns "" on any failure
83
+ * (fail-open: your agent must never break because Era is unreachable).
84
+ * The last message must be from the user.
85
+ */
86
+ async turnContext(messages, options = {}) {
87
+ const turns = messagesToTurns(messages);
88
+ if (turns.length === 0 || turns[turns.length - 1].role !== "user")
89
+ return "";
90
+ const userId = options.userId ?? this.opts.userId ?? "anonymous";
91
+ const sessionId = options.sessionId ?? this.opts.sessionId ?? (await defaultSessionId(turns));
92
+ const key = `${userId}|${sessionId}|${turns[turns.length - 1].turn_index}`;
93
+ if (this.lastTurn?.key === key)
94
+ return this.lastTurn.block;
95
+ const block = this.fetchTurn(turns, userId, sessionId);
96
+ this.lastTurn = { key, block };
97
+ return block;
98
+ }
99
+ async fetchTurn(turns, userId, sessionId) {
100
+ const mode = this.opts.mode ?? "async";
101
+ const timeoutMs = this.opts.timeoutMs ?? (mode === "sync" ? ERA_SYNC_TIMEOUT_MS : ERA_ASYNC_TIMEOUT_MS);
102
+ const maxRetries = Math.max(0, this.opts.maxRetries ?? DEFAULT_MAX_RETRIES);
103
+ const body = JSON.stringify({
104
+ partner_id: this.opts.partnerId,
105
+ user_id: userId,
106
+ session_id: sessionId,
107
+ mode,
108
+ brand_voice: this.opts.brandVoice,
109
+ turns,
110
+ });
111
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
112
+ try {
113
+ const resp = await fetch(`${this.opts.baseUrl}/v1/era/turn`, {
114
+ method: "POST",
115
+ headers: { "Content-Type": "application/json", "X-API-Key": this.opts.apiKey },
116
+ body,
117
+ signal: AbortSignal.timeout(timeoutMs),
118
+ });
119
+ if (resp.ok) {
120
+ const data = (await resp.json());
121
+ const block = data.block ?? "";
122
+ this.log.info(`block ${block ? "received" : "empty"} | mode=${mode} freshness=${data.freshness} chars=${block.length}`);
123
+ return block;
124
+ }
125
+ if (resp.status >= 400 && resp.status < 500) {
126
+ // Config/usage error — retrying won't help; fail open loudly.
127
+ this.log.warn(`request rejected (${resp.status}), failing open`);
128
+ return "";
129
+ }
130
+ // 5xx — transient; retry if budget remains.
131
+ if (attempt < maxRetries) {
132
+ await sleep(RETRY_BACKOFF_BASE_MS * 2 ** attempt);
133
+ continue;
134
+ }
135
+ this.log.warn(`server error ${resp.status}, failing open`);
136
+ return "";
137
+ }
138
+ catch (err) {
139
+ // Timeout / network error — transient; retry if budget remains.
140
+ if (attempt < maxRetries) {
141
+ await sleep(RETRY_BACKOFF_BASE_MS * 2 ** attempt);
142
+ continue;
143
+ }
144
+ this.log.warn(`unavailable, failing open: ${err instanceof Error ? err.message : err}`);
145
+ return "";
146
+ }
147
+ }
148
+ return "";
149
+ }
150
+ }
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@ay-2814/era-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Era cognitive context for any conversational AI agent",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ },
11
+ "./ai-sdk": {
12
+ "types": "./dist/ai-sdk.d.ts",
13
+ "default": "./dist/ai-sdk.js"
14
+ }
15
+ },
16
+ "license": "UNLICENSED",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "sideEffects": false,
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "test": "node --test \"test/*.test.mjs\"",
30
+ "prepublishOnly": "npm run build && npm test"
31
+ },
32
+ "peerDependencies": {
33
+ "ai": ">=5"
34
+ },
35
+ "peerDependenciesMeta": {
36
+ "ai": {
37
+ "optional": true
38
+ }
39
+ },
40
+ "devDependencies": {
41
+ "ai": "^6.0.116",
42
+ "typescript": "^5.9.0"
43
+ }
44
+ }