@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 +53 -0
- package/dist/ai-sdk.d.ts +4 -0
- package/dist/ai-sdk.js +55 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +150 -0
- package/package.json +44 -0
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
|
+
```
|
package/dist/ai-sdk.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|