@hexis-ai/engram-sdk 0.10.0 → 0.11.1

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.
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Buffered / fire-and-forget telemetry layer for `Engram`.
3
+ *
4
+ * Modelled after the Langfuse SDK ergonomics so existing observability
5
+ * code translates one-for-one:
6
+ *
7
+ * import { Engram } from "@hexis-ai/engram-sdk";
8
+ * const engram = new Engram({ baseUrl, apiKey, flushIntervalMs: 5000 });
9
+ *
10
+ * engram.session({ id: convId, channel: "slack_dm" });
11
+ * engram.message({ sessionId: convId, role: "user", content: "hi" });
12
+ * engram.message({ sessionId: convId, role: "assistant", content: out, model });
13
+ * engram.event({ sessionId: convId, type: "title", title: "Trip planning" });
14
+ *
15
+ * await engram.flush(); // process exit
16
+ * await engram.shutdown();
17
+ *
18
+ * Guarantees:
19
+ * - Every method returns synchronously (no `await` required at the call site).
20
+ * - Errors are surfaced via the `onError` hook configured on the Engram
21
+ * client; nothing throws to the caller.
22
+ * - The first message() for a session implicitly waits for the
23
+ * corresponding POST /v1/sessions to complete (per-session ready
24
+ * promise), so the host doesn't need to await session() before
25
+ * starting to record events.
26
+ * - When `baseUrl` is unset on the parent Engram, the buffer is a
27
+ * no-op (the Engram constructor would have thrown earlier, so this
28
+ * is enforced upstream; here we just document the intent).
29
+ */
30
+ import type { Engram } from "./client";
31
+ import type { MessageContentBlock, SessionEvent, SessionInit } from "./types";
32
+ export interface TelemetrySession extends Omit<SessionInit, "id"> {
33
+ /** Host-supplied session id. Required for telemetry mode. */
34
+ id: string;
35
+ }
36
+ export interface TelemetryMessage {
37
+ sessionId: string;
38
+ role: string;
39
+ /** Plain string is wrapped into a single `text` block; arrays pass through. */
40
+ content: string | MessageContentBlock[];
41
+ at?: string;
42
+ model?: string;
43
+ tokens?: {
44
+ input: number;
45
+ output: number;
46
+ };
47
+ /** Stable host-side id (mirrors engram's MessageEvent.message_id). */
48
+ message_id?: string;
49
+ }
50
+ export type TelemetryEvent = {
51
+ sessionId: string;
52
+ type: "participant";
53
+ personId: string;
54
+ at?: string;
55
+ } | {
56
+ sessionId: string;
57
+ type: "title";
58
+ title: string;
59
+ at?: string;
60
+ } | {
61
+ sessionId: string;
62
+ type: "step";
63
+ tool: string;
64
+ input?: Record<string, unknown>;
65
+ result?: unknown;
66
+ resources?: string[];
67
+ at?: string;
68
+ } | {
69
+ sessionId: string;
70
+ type: "end";
71
+ at?: string;
72
+ };
73
+ /**
74
+ * Internal coordinator that owns per-session buffers and gates flushes
75
+ * on session-creation acks. Exposed on the parent Engram via
76
+ * `engram.session() / .message() / .event() / .flush() / .shutdown()`.
77
+ */
78
+ export declare class BufferedTelemetry {
79
+ private readonly engram;
80
+ private readonly handles;
81
+ constructor(engram: Engram);
82
+ /**
83
+ * Observe a new (or already-known) session. Idempotent for the same
84
+ * id — subsequent calls with new metadata are folded into an
85
+ * updateSession() rather than re-creating.
86
+ */
87
+ session(input: TelemetrySession): void;
88
+ /**
89
+ * Observe a message turn. Lazily creates a session handle if none
90
+ * exists yet — but without a prior `session({id, ...})` call the
91
+ * subsequent flush will fail (engram-server returns 404) and the
92
+ * error surfaces via onError. Pass session() first.
93
+ */
94
+ message(input: TelemetryMessage): void;
95
+ /** Observe an arbitrary session event (participant / title / step / end). */
96
+ event(input: TelemetryEvent): void;
97
+ /**
98
+ * Drain every per-session buffer in parallel. Safe to call at any
99
+ * point; failures route to onError, never thrown.
100
+ */
101
+ flush(): Promise<void>;
102
+ /**
103
+ * Final flush + stop timers. After shutdown the buffer is closed and
104
+ * further telemetry calls drop with onError.
105
+ */
106
+ shutdown(): Promise<void>;
107
+ /** Test helper. Drops all in-memory state without flushing. */
108
+ resetForTests(): void;
109
+ }
110
+ export type { SessionEvent };
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Buffered / fire-and-forget telemetry layer for `Engram`.
3
+ *
4
+ * Modelled after the Langfuse SDK ergonomics so existing observability
5
+ * code translates one-for-one:
6
+ *
7
+ * import { Engram } from "@hexis-ai/engram-sdk";
8
+ * const engram = new Engram({ baseUrl, apiKey, flushIntervalMs: 5000 });
9
+ *
10
+ * engram.session({ id: convId, channel: "slack_dm" });
11
+ * engram.message({ sessionId: convId, role: "user", content: "hi" });
12
+ * engram.message({ sessionId: convId, role: "assistant", content: out, model });
13
+ * engram.event({ sessionId: convId, type: "title", title: "Trip planning" });
14
+ *
15
+ * await engram.flush(); // process exit
16
+ * await engram.shutdown();
17
+ *
18
+ * Guarantees:
19
+ * - Every method returns synchronously (no `await` required at the call site).
20
+ * - Errors are surfaced via the `onError` hook configured on the Engram
21
+ * client; nothing throws to the caller.
22
+ * - The first message() for a session implicitly waits for the
23
+ * corresponding POST /v1/sessions to complete (per-session ready
24
+ * promise), so the host doesn't need to await session() before
25
+ * starting to record events.
26
+ * - When `baseUrl` is unset on the parent Engram, the buffer is a
27
+ * no-op (the Engram constructor would have thrown earlier, so this
28
+ * is enforced upstream; here we just document the intent).
29
+ */
30
+ import { EngramSession } from "./client";
31
+ // --- BufferedTelemetry ---------------------------------------------------
32
+ /**
33
+ * Internal coordinator that owns per-session buffers and gates flushes
34
+ * on session-creation acks. Exposed on the parent Engram via
35
+ * `engram.session() / .message() / .event() / .flush() / .shutdown()`.
36
+ */
37
+ export class BufferedTelemetry {
38
+ engram;
39
+ handles = new Map();
40
+ constructor(engram) {
41
+ this.engram = engram;
42
+ }
43
+ /**
44
+ * Observe a new (or already-known) session. Idempotent for the same
45
+ * id — subsequent calls with new metadata are folded into an
46
+ * updateSession() rather than re-creating.
47
+ */
48
+ session(input) {
49
+ const existing = this.handles.get(input.id);
50
+ if (existing) {
51
+ existing.update(input);
52
+ return;
53
+ }
54
+ this.handles.set(input.id, new BufferedSession(this.engram, input));
55
+ }
56
+ /**
57
+ * Observe a message turn. Lazily creates a session handle if none
58
+ * exists yet — but without a prior `session({id, ...})` call the
59
+ * subsequent flush will fail (engram-server returns 404) and the
60
+ * error surfaces via onError. Pass session() first.
61
+ */
62
+ message(input) {
63
+ const handle = this.handles.get(input.sessionId);
64
+ if (!handle) {
65
+ this.engram.config.onError(new Error(`engram telemetry: message() for unknown session ${input.sessionId}; call session({id}) first`));
66
+ return;
67
+ }
68
+ handle.message(input);
69
+ }
70
+ /** Observe an arbitrary session event (participant / title / step / end). */
71
+ event(input) {
72
+ const handle = this.handles.get(input.sessionId);
73
+ if (!handle) {
74
+ this.engram.config.onError(new Error(`engram telemetry: event() for unknown session ${input.sessionId}; call session({id}) first`));
75
+ return;
76
+ }
77
+ handle.event(input);
78
+ }
79
+ /**
80
+ * Drain every per-session buffer in parallel. Safe to call at any
81
+ * point; failures route to onError, never thrown.
82
+ */
83
+ async flush() {
84
+ await Promise.all([...this.handles.values()].map((h) => h.flush().catch((e) => this.engram.config.onError(e))));
85
+ }
86
+ /**
87
+ * Final flush + stop timers. After shutdown the buffer is closed and
88
+ * further telemetry calls drop with onError.
89
+ */
90
+ async shutdown() {
91
+ const all = [...this.handles.values()];
92
+ this.handles.clear();
93
+ await Promise.all(all.map((h) => h.shutdown().catch((e) => this.engram.config.onError(e))));
94
+ }
95
+ /** Test helper. Drops all in-memory state without flushing. */
96
+ resetForTests() {
97
+ this.handles.clear();
98
+ }
99
+ }
100
+ // --- Per-session buffer --------------------------------------------------
101
+ /**
102
+ * One BufferedSession per session id. Wraps the existing EngramSession
103
+ * (which already implements batched flush + retry) and gates the first
104
+ * flush on the session's create ack.
105
+ */
106
+ class BufferedSession {
107
+ engram;
108
+ id;
109
+ /**
110
+ * Resolves to `true` when POST /v1/sessions acked, `false` on
111
+ * transport failure. Never rejects — preserves Bun's strict
112
+ * unhandled-rejection contract even when callers don't await
113
+ * the buffer (Langfuse-style fire-and-forget).
114
+ */
115
+ ready;
116
+ inner;
117
+ ended = false;
118
+ constructor(engram, init) {
119
+ this.engram = engram;
120
+ this.id = init.id;
121
+ this.inner = new EngramSession(engram, init.id);
122
+ this.ready = engram
123
+ .startSessionWithoutHandle(init)
124
+ .then(() => true)
125
+ .catch((e) => {
126
+ engram.config.onError(e);
127
+ return false;
128
+ });
129
+ }
130
+ message(input) {
131
+ try {
132
+ this.inner.recordMessage({
133
+ role: input.role,
134
+ content: normalizeContent(input.content),
135
+ ...(input.at !== undefined ? { at: input.at } : {}),
136
+ ...(input.model !== undefined ? { model: input.model } : {}),
137
+ ...(input.tokens !== undefined ? { tokens: input.tokens } : {}),
138
+ ...(input.message_id !== undefined ? { message_id: input.message_id } : {}),
139
+ });
140
+ }
141
+ catch (e) {
142
+ this.engram.config.onError(e);
143
+ }
144
+ }
145
+ event(input) {
146
+ try {
147
+ switch (input.type) {
148
+ case "participant":
149
+ this.inner.addParticipant(input.personId);
150
+ break;
151
+ case "title":
152
+ this.inner.setTitle(input.title);
153
+ break;
154
+ case "step":
155
+ this.inner.recordStep({
156
+ tool: input.tool,
157
+ ...(input.resources !== undefined ? { resources: input.resources } : {}),
158
+ ...(input.input !== undefined ? { input: input.input } : {}),
159
+ ...(input.result !== undefined ? { result: input.result } : {}),
160
+ });
161
+ break;
162
+ case "end":
163
+ void this.shutdown().catch((e) => this.engram.config.onError(e));
164
+ break;
165
+ }
166
+ }
167
+ catch (e) {
168
+ this.engram.config.onError(e);
169
+ }
170
+ }
171
+ /**
172
+ * Merge updated session metadata via the patch endpoint. Fire-and-
173
+ * forget. Useful when a session was observed earlier with partial
174
+ * info and the host now has more (e.g. resolved channel later).
175
+ */
176
+ update(input) {
177
+ const patch = {};
178
+ if (input.title !== undefined)
179
+ patch.title = input.title;
180
+ if (input.channel !== undefined)
181
+ patch.channel = input.channel;
182
+ if (input.status !== undefined)
183
+ patch.status = input.status;
184
+ if (input.summary !== undefined)
185
+ patch.summary = input.summary;
186
+ if (input.model !== undefined)
187
+ patch.model = input.model;
188
+ if (input.trigger_conversation_id !== undefined)
189
+ patch.trigger_conversation_id = input.trigger_conversation_id;
190
+ if (input.trigger_event_id !== undefined)
191
+ patch.trigger_event_id = input.trigger_event_id;
192
+ if (Object.keys(patch).length === 0)
193
+ return;
194
+ void this.ready.then((ok) => {
195
+ if (!ok)
196
+ return;
197
+ return this.engram
198
+ .updateSession(this.id, patch)
199
+ .catch((e) => this.engram.config.onError(e));
200
+ });
201
+ }
202
+ async flush() {
203
+ const ok = await this.ready;
204
+ if (!ok)
205
+ return;
206
+ await this.inner.flush().catch((e) => this.engram.config.onError(e));
207
+ }
208
+ async shutdown() {
209
+ if (this.ended)
210
+ return;
211
+ this.ended = true;
212
+ const ok = await this.ready;
213
+ if (!ok)
214
+ return;
215
+ await this.inner.end().catch((e) => this.engram.config.onError(e));
216
+ }
217
+ }
218
+ // --- Helpers --------------------------------------------------------------
219
+ function normalizeContent(content) {
220
+ if (typeof content === "string") {
221
+ return content.length > 0 ? [{ type: "text", text: content }] : [];
222
+ }
223
+ return content;
224
+ }
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { ScoredSession, SearchOptions, Session, SessionStep } from "@hexis-ai/engram-core";
2
2
  import { type RefCandidate } from "./extract";
3
3
  import type { AliasInfo, AliasUpsert, EventBatch, IdentityInfo, IdentityUpsert, MessageContentBlock, PersonCreate, PersonInfo, PersonMap, PersonUpdate, SessionEvent, SessionInit, SessionUpdate } from "./types";
4
+ import { type TelemetryEvent, type TelemetryMessage, type TelemetrySession } from "./buffered";
4
5
  /**
5
6
  * Envelope returned by session endpoints. The persons map is deduped
6
7
  * across whatever sessions the response carries so display info isn't
@@ -93,13 +94,48 @@ export declare class Engram {
93
94
  private readonly authHeaders?;
94
95
  readonly maxRetries: number;
95
96
  readonly retryBackoffMs: number;
97
+ private telemetry;
96
98
  constructor(opts: EngramOptions);
99
+ /**
100
+ * Langfuse-style fire-and-forget telemetry surface. Lazily initialised
101
+ * so apps that never observe (search-only consumers) pay no overhead.
102
+ */
103
+ private get bufferedTelemetry();
104
+ /**
105
+ * Observe a new (or already-known) session. Idempotent for the same id.
106
+ * Fire-and-forget — returns synchronously, transport failures route
107
+ * to `onError`.
108
+ */
109
+ session(input: TelemetrySession): void;
110
+ /**
111
+ * Observe a message turn against an existing session. Call
112
+ * `session({id})` first so the create ack is queued.
113
+ */
114
+ message(input: TelemetryMessage): void;
115
+ /**
116
+ * Observe an arbitrary session event (participant added, title set,
117
+ * tool step, end). Same fire-and-forget contract.
118
+ */
119
+ event(input: TelemetryEvent): void;
120
+ /**
121
+ * Drain every per-session buffer. Call before short-lived processes
122
+ * exit; long-lived servers can rely on auto-flush (flushIntervalMs).
123
+ */
124
+ flush(): Promise<void>;
125
+ /** Final flush + stop. Use at process exit. */
126
+ shutdown(): Promise<void>;
97
127
  /** Probe identity — returns the workspace the configured key resolves to. */
98
128
  me(): Promise<{
99
129
  workspaceId: string;
100
130
  }>;
101
131
  /** Begin a new session. Returns a handle for buffering events. */
102
132
  startSession(init?: SessionInit): Promise<EngramSession>;
133
+ /**
134
+ * POST /v1/sessions without constructing a handle. Used by
135
+ * BufferedTelemetry, which manages its own per-session handles and
136
+ * just needs the create ack.
137
+ */
138
+ startSessionWithoutHandle(init: SessionInit): Promise<void>;
103
139
  /** Fetch a single session by id, plus the persons map for its participants/viewers. */
104
140
  getSession(id: string): Promise<SessionEnvelope>;
105
141
  /**
package/dist/client.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { encodeResourceId, extractReferences } from "./extract";
2
2
  import { parseToolName } from "./tool-name";
3
+ import { BufferedTelemetry, } from "./buffered";
3
4
  export class Engram {
4
5
  apiKey;
5
6
  baseUrl;
@@ -11,6 +12,7 @@ export class Engram {
11
12
  authHeaders;
12
13
  maxRetries;
13
14
  retryBackoffMs;
15
+ telemetry = null;
14
16
  constructor(opts) {
15
17
  if (!opts.apiKey)
16
18
  throw new Error("Engram: apiKey is required");
@@ -27,6 +29,53 @@ export class Engram {
27
29
  this.maxRetries = opts.maxRetries ?? 4;
28
30
  this.retryBackoffMs = opts.retryBackoffMs ?? 500;
29
31
  }
32
+ /**
33
+ * Langfuse-style fire-and-forget telemetry surface. Lazily initialised
34
+ * so apps that never observe (search-only consumers) pay no overhead.
35
+ */
36
+ get bufferedTelemetry() {
37
+ if (!this.telemetry)
38
+ this.telemetry = new BufferedTelemetry(this);
39
+ return this.telemetry;
40
+ }
41
+ /**
42
+ * Observe a new (or already-known) session. Idempotent for the same id.
43
+ * Fire-and-forget — returns synchronously, transport failures route
44
+ * to `onError`.
45
+ */
46
+ session(input) {
47
+ this.bufferedTelemetry.session(input);
48
+ }
49
+ /**
50
+ * Observe a message turn against an existing session. Call
51
+ * `session({id})` first so the create ack is queued.
52
+ */
53
+ message(input) {
54
+ this.bufferedTelemetry.message(input);
55
+ }
56
+ /**
57
+ * Observe an arbitrary session event (participant added, title set,
58
+ * tool step, end). Same fire-and-forget contract.
59
+ */
60
+ event(input) {
61
+ this.bufferedTelemetry.event(input);
62
+ }
63
+ /**
64
+ * Drain every per-session buffer. Call before short-lived processes
65
+ * exit; long-lived servers can rely on auto-flush (flushIntervalMs).
66
+ */
67
+ async flush() {
68
+ if (!this.telemetry)
69
+ return;
70
+ await this.telemetry.flush();
71
+ }
72
+ /** Final flush + stop. Use at process exit. */
73
+ async shutdown() {
74
+ if (!this.telemetry)
75
+ return;
76
+ await this.telemetry.shutdown();
77
+ this.telemetry = null;
78
+ }
30
79
  /** Probe identity — returns the workspace the configured key resolves to. */
31
80
  async me() {
32
81
  return this.request("GET", "/v1/me");
@@ -36,6 +85,14 @@ export class Engram {
36
85
  const ack = await this.request("POST", "/v1/sessions", init);
37
86
  return new EngramSession(this, ack.id);
38
87
  }
88
+ /**
89
+ * POST /v1/sessions without constructing a handle. Used by
90
+ * BufferedTelemetry, which manages its own per-session handles and
91
+ * just needs the create ack.
92
+ */
93
+ async startSessionWithoutHandle(init) {
94
+ await this.request("POST", "/v1/sessions", init);
95
+ }
39
96
  /** Fetch a single session by id, plus the persons map for its participants/viewers. */
40
97
  async getSession(id) {
41
98
  return this.request("GET", `/v1/sessions/${encodeURIComponent(id)}`);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { Engram, EngramSession, type EngramOptions, type RecordStepInput, type SearchRequest, type SearchResponse, type SearchEnvelope, type SessionEnvelope, type SessionListEnvelope, } from "./client";
2
2
  export { extractReferences, encodeResourceId, type RefCandidate, type ReferenceService, type ReferenceAction, } from "./extract";
3
3
  export { parseToolName, type ParsedToolName } from "./tool-name";
4
+ export { BufferedTelemetry, type TelemetrySession, type TelemetryMessage, type TelemetryEvent, } from "./buffered";
4
5
  export { fetchIdToken, cloudRunIdTokenAuth } from "./id-token";
5
6
  export { EngramAdmin, createAdminClient, type AdminClientOptions, type CreateWorkspaceInput, type CreateWorkspaceResult, type Workspace as AdminWorkspace, type ApiKey as AdminApiKey, type IssuedKey as AdminIssuedKey, } from "./admin";
6
7
  export type { SessionInit, SessionUpdate, SessionAck, SessionEvent, StepEvent, ParticipantEvent, TitleEvent, EndEvent, MessageContentBlock, MessageEvent, EventBatch, PersonInfo, PersonCreate, PersonUpdate, PersonMap, AliasInfo, AliasUpsert, IdentityInfo, IdentityUpsert, } from "./types";
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { Engram, EngramSession, } from "./client";
2
2
  export { extractReferences, encodeResourceId, } from "./extract";
3
3
  export { parseToolName } from "./tool-name";
4
+ export { BufferedTelemetry, } from "./buffered";
4
5
  export { fetchIdToken, cloudRunIdTokenAuth } from "./id-token";
5
6
  export { EngramAdmin, createAdminClient, } from "./admin";
package/package.json CHANGED
@@ -1,27 +1,16 @@
1
1
  {
2
2
  "name": "@hexis-ai/engram-sdk",
3
- "version": "0.10.0",
4
- "description": "Host SDK for engram. Records agent session steps and ships them to an engram server.",
5
- "keywords": [
6
- "engram",
7
- "agents",
8
- "claude",
9
- "anthropic",
10
- "sdk",
11
- "observability"
12
- ],
13
- "homepage": "https://github.com/hexis-ltd/engram#readme",
3
+ "version": "0.11.1",
4
+ "author": "hexis ltd.",
14
5
  "repository": {
15
6
  "type": "git",
16
7
  "url": "git+https://github.com/hexis-ltd/engram.git",
17
8
  "directory": "packages/sdk"
18
9
  },
19
- "bugs": "https://github.com/hexis-ltd/engram/issues",
20
- "author": "hexis ltd.",
21
- "license": "MIT",
22
- "type": "module",
23
10
  "main": "dist/index.js",
24
- "types": "dist/index.d.ts",
11
+ "dependencies": {
12
+ "@hexis-ai/engram-core": "^0.2.0"
13
+ },
25
14
  "exports": {
26
15
  ".": {
27
16
  "types": "./dist/index.d.ts",
@@ -36,19 +25,30 @@
36
25
  "default": "./dist/tool-name.js"
37
26
  }
38
27
  },
28
+ "bugs": "https://github.com/hexis-ltd/engram/issues",
29
+ "description": "Host SDK for engram. Records agent session steps and ships them to an engram server.",
39
30
  "files": [
40
31
  "dist"
41
32
  ],
33
+ "homepage": "https://github.com/hexis-ltd/engram#readme",
34
+ "keywords": [
35
+ "engram",
36
+ "agents",
37
+ "claude",
38
+ "anthropic",
39
+ "sdk",
40
+ "observability"
41
+ ],
42
+ "license": "MIT",
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
42
46
  "scripts": {
43
47
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
44
48
  "pack": "bun run build && bun pm pack",
45
49
  "test": "bun test",
46
50
  "type-check": "tsc --noEmit"
47
51
  },
48
- "dependencies": {
49
- "@hexis-ai/engram-core": "^0.2.0"
50
- },
51
- "publishConfig": {
52
- "access": "public"
53
- }
52
+ "type": "module",
53
+ "types": "dist/index.d.ts"
54
54
  }