@telemai/sdk 0.1.1 → 0.2.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.
package/README.md CHANGED
@@ -42,7 +42,7 @@ See [Authentication](https://docs.telem.ai/authentication/) for the full rules.
42
42
 
43
43
  ```ts
44
44
 
45
- const telem = new Telem() // environment, then ~/.telem/credentials.json
45
+ const telem = new Telem // environment, then ~/.telem/credentials.json
46
46
  const telem = new Telem("tlm_...") // just a key
47
47
  const telem = new Telem({
48
48
  apiKey: "tlm_...",
@@ -176,7 +176,7 @@ Do not expect a shorter list. The options are on the
176
176
  List the providers the deployment can run:
177
177
 
178
178
  ```ts
179
- for (const provider of await telem.providers()) {
179
+ for (const provider of await telem.providers) {
180
180
  console.log(provider.name, provider.activeByDefault, provider.tiers)
181
181
  }
182
182
  ```
@@ -187,7 +187,7 @@ Every search belongs to a session. The response carries its id as `sessionId`. R
187
187
  your sessions back to see what an agent searched for. Both methods require an API key:
188
188
 
189
189
  ```ts
190
- for (const session of await telem.sessions.list()) {
190
+ for (const session of await telem.sessions.list) {
191
191
  console.log(session.id, session.interactionCount, session.latestInteractionAt)
192
192
  }
193
193
 
@@ -198,8 +198,38 @@ console.log(summary.query, summary.goal, summary.context)
198
198
  `results` returns the query, the goal and the context of the session, plus the
199
199
  results of each search step.
200
200
 
201
+ ## Vercel AI SDK
202
+
203
+ If your agent runs on the Vercel AI SDK, `@telemai/sdk/vercel` gives it
204
+ `telem_search` and `telem_fetch` tools with query lineage attached to every call,
205
+ subagents included. Build the tool set once per conversation and pass it to
206
+ `generateText`:
207
+
208
+ ```ts
209
+ import { generateText } from "ai"
210
+ import Telem from "@telemai/sdk"
211
+ import { createTelemVercelTools } from "@telemai/sdk/vercel"
212
+
213
+ const telem = new Telem
214
+ const tools = createTelemVercelTools({ telem, harness: "my-app" })
215
+
216
+ const result = await generateText({ model, tools, prompt: "Is the Nintendo Museum open over New Year?" })
217
+ ```
218
+
219
+ A subagent is a `generateText` started inside one of your tools. Pass that tool to
220
+ `createTelemVercelTools` and use the same tool set inside the subagent; its calls
221
+ then appear in the console under the tool call that started it. `ai` version 5
222
+ or later is an optional peer dependency, used by this subpath only. The full
223
+ guide, subagents, your own tools, and the options, is on the
224
+ [Vercel AI SDK page](https://docs.telem.ai/integrations/vercel-ai/). The runnable example is `examples/vercel-ai-agents.mjs`
225
+ in the SDK source, first in the [examples list](https://github.com/TelemAI/telem-js-sdk/tree/main/examples).
226
+
201
227
  ## Query lineage
202
228
 
229
+ If your agent uses the Vercel AI SDK, use
230
+ [`@telemai/sdk/vercel`](https://docs.telem.ai/integrations/vercel-ai/). It attaches lineage data to
231
+ every call by itself, subagents included, and you build nothing below by hand.
232
+
203
233
  Every search and fetch from an agent can carry **lineage data**. Lineage data tells
204
234
  Telem which conversation made the call, and where in the conversation it happened.
205
235
  The console then shows all the calls of one conversation together, in order, and
@@ -221,8 +251,8 @@ Node SDK, where the completion id is the turn and each tool call has its own id:
221
251
 
222
252
  ```ts
223
253
 
224
- const telem = new Telem()
225
- const openai = new OpenAI()
254
+ const telem = new Telem
255
+ const openai = new OpenAI
226
256
  const messages = [{ role: "user" as const, content: "Is the Nintendo Museum open over New Year?" }]
227
257
  const tools = [
228
258
  {
@@ -239,26 +269,36 @@ const tools = [
239
269
  },
240
270
  ]
241
271
 
242
- const conversationId = crypto.randomUUID() // once, when the conversation starts
272
+ const conversationId = crypto.randomUUID // once, when the conversation starts
243
273
  const windowId = await windowAnchor(messages) // once, from the first message
244
274
 
245
275
  const completion = await openai.chat.completions.create({ model: "gpt-4o", messages, tools })
246
276
  for (const call of completion.choices[0].message.tool_calls ?? []) {
247
277
  if (call.type !== "function") continue
278
+ const { query, goal, context } = JSON.parse(call.function.arguments)
248
279
  const lineage = await buildMetadata({
249
280
  harness: "my-app", // a stable name for your integration
250
281
  conversationId,
251
282
  windowId,
252
283
  messageId: completion.id, // this model turn
253
284
  toolCallId: call.id, // this tool call
254
- history: messages, // [{ role, content }]: the conversation so far
285
+ history: [ // [{ role, content }]: the conversation so far, ending with this call
286
+ ...messages,
287
+ { role: "assistant", content: toolMarker("telem_search", "running", { query, goal, context }) },
288
+ ],
255
289
  ancestors: [], // the parent snapshot; [] for the first agent
256
290
  })
257
- const { query, goal, context } = JSON.parse(call.function.arguments)
258
291
  const response = await telem.search(query, withTrajectory({ goal, context }, lineage))
259
292
  }
260
293
  ```
261
294
 
295
+ `history` has a rule the console depends on, and the example follows it on its
296
+ `history` line: write each tool call into the history as a marker with
297
+ `toolMarker(name, status, input)`, and end the history of every call with the
298
+ call being made, marked `running`. When a call completes, keep its marker in the
299
+ history as `completed`. The console reads the conversation's goals from those
300
+ markers, so build them with the helper rather than by hand.
301
+
262
302
  `withTrajectory` attaches the lineage data to one call. Use it the same way with
263
303
  `telem.fetch`. When lineage data is present, the client ignores a `session` id.
264
304
 
@@ -0,0 +1,373 @@
1
+ // src/errors.ts
2
+ var TelemError = class extends Error {
3
+ status;
4
+ code;
5
+ details;
6
+ constructor(message, options = {}) {
7
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
8
+ this.name = "TelemError";
9
+ this.status = options.status;
10
+ this.code = options.code;
11
+ this.details = options.details;
12
+ }
13
+ };
14
+ var TimeoutError = class extends TelemError {
15
+ constructor(message, cause) {
16
+ super(message, { code: "timeout", cause });
17
+ this.name = "TimeoutError";
18
+ }
19
+ };
20
+
21
+ // src/http.ts
22
+ var MAX_DELAY_MS = 3e4;
23
+ var MAX_TIMEOUT_MS = 2 ** 31 - 1;
24
+ var BASE_DELAY_MS = 500;
25
+ function isObject(value) {
26
+ return typeof value === "object" && value !== null && !Array.isArray(value);
27
+ }
28
+ function asObject(value) {
29
+ return isObject(value) ? value : {};
30
+ }
31
+ function asArray(value) {
32
+ return Array.isArray(value) ? value : [];
33
+ }
34
+ function invalidRequest(message) {
35
+ return new TelemError(message, { code: "invalid_request" });
36
+ }
37
+ function stringList(items, empty, what) {
38
+ const list = asArray(items);
39
+ if (!list.length) throw invalidRequest(empty);
40
+ for (const item of list) {
41
+ if (typeof item !== "string") {
42
+ throw invalidRequest(`${what} must be strings, got ${item === null ? "null" : typeof item}`);
43
+ }
44
+ if (item === "") throw invalidRequest(`${what} must not be empty`);
45
+ }
46
+ return list;
47
+ }
48
+ function errorEnvelope(body) {
49
+ if (!isObject(body)) return void 0;
50
+ if (isObject(body.error)) return body.error;
51
+ if (isObject(body.detail)) return body.detail;
52
+ return void 0;
53
+ }
54
+ function detailFromBody(body) {
55
+ if (!isObject(body)) return void 0;
56
+ const envelope = errorEnvelope(body);
57
+ if (envelope && typeof envelope.message === "string" && envelope.message) {
58
+ return envelope.message;
59
+ }
60
+ const detail = body.detail;
61
+ if (typeof detail === "string" && detail) return detail;
62
+ if (Array.isArray(detail) && detail.length) {
63
+ const messages = detail.map((item) => isObject(item) && typeof item.msg === "string" ? item.msg : void 0).filter((message) => message !== void 0);
64
+ return messages.join(", ") || JSON.stringify(detail);
65
+ }
66
+ return void 0;
67
+ }
68
+ function codeFromBody(body) {
69
+ const envelope = errorEnvelope(body);
70
+ return envelope && typeof envelope.code === "string" ? envelope.code : void 0;
71
+ }
72
+ function isResponseLike(value) {
73
+ return isObject(value) && typeof value.text === "function" && typeof value.status === "number" && typeof value.headers?.get === "function";
74
+ }
75
+ var NotAResponse = class extends Error {
76
+ };
77
+ function defaultMessage(status) {
78
+ if (status === 400) return "Bad request";
79
+ if (status === 401 || status === 403) return "Authentication failed";
80
+ if (status === 404) return "Not found";
81
+ return `Unexpected status code ${status}`;
82
+ }
83
+ function statusError(status, body) {
84
+ return new TelemError(detailFromBody(body) ?? defaultMessage(status), {
85
+ status,
86
+ code: codeFromBody(body),
87
+ details: body
88
+ });
89
+ }
90
+ function connectionError(url, cause) {
91
+ const error = cause;
92
+ const detail = typeof error?.cause?.message === "string" && error.cause.message || typeof error?.message === "string" && error.message || String(cause);
93
+ return new TelemError(`could not reach ${url}: ${detail}`, {
94
+ code: "connection_error",
95
+ cause
96
+ });
97
+ }
98
+ function isRetryable(status) {
99
+ return status >= 500 || status === 429;
100
+ }
101
+ function backoff(attempt) {
102
+ return Math.min(BASE_DELAY_MS * 2 ** attempt, MAX_DELAY_MS);
103
+ }
104
+ function retryDelay(retryAfter, attempt) {
105
+ if (retryAfter !== null && /^\s*\d+\s*$/.test(retryAfter)) {
106
+ return Math.min(Number(retryAfter) * 1e3, MAX_DELAY_MS);
107
+ }
108
+ return backoff(attempt);
109
+ }
110
+ async function request(config, method, path, body, abort) {
111
+ const url = `${config.baseUrl}/${path.replace(/^\/+/, "")}`;
112
+ const headers = { "User-Agent": config.userAgent };
113
+ if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
114
+ const init = { method, headers };
115
+ if (body !== void 0) {
116
+ headers["Content-Type"] = "application/json";
117
+ try {
118
+ init.body = JSON.stringify(body);
119
+ } catch (cause) {
120
+ throw invalidRequest(`request body is not JSON: ${cause?.message ?? String(cause)}`);
121
+ }
122
+ }
123
+ let onAbort = () => {
124
+ };
125
+ const abortion = abort && new Promise((_, reject) => {
126
+ onAbort = () => reject(abort.reason ?? new Error("aborted"));
127
+ abort.addEventListener("abort", onAbort, { once: true });
128
+ });
129
+ abortion?.catch(() => {
130
+ });
131
+ const pause = (ms) => abortion ? Promise.race([config.sleep(ms), abortion]) : config.sleep(ms);
132
+ try {
133
+ return await attempts();
134
+ } finally {
135
+ abort?.removeEventListener("abort", onAbort);
136
+ }
137
+ async function attempts() {
138
+ for (let attempt = 0; ; attempt++) {
139
+ if (abort?.aborted) throw abort.reason ?? new Error("aborted");
140
+ const timeout = Number.isFinite(config.timeoutMs) ? AbortSignal.timeout(Math.min(config.timeoutMs, MAX_TIMEOUT_MS)) : void 0;
141
+ const signal = timeout && abort ? typeof AbortSignal.any === "function" ? AbortSignal.any([timeout, abort]) : timeout : timeout ?? abort;
142
+ let onAbort2 = () => {
143
+ };
144
+ const deadline = signal && new Promise((_, reject) => {
145
+ onAbort2 = () => reject(signal.reason);
146
+ signal.addEventListener("abort", onAbort2, { once: true });
147
+ });
148
+ const race = (work) => deadline ? Promise.race([work, deadline]) : work;
149
+ let received;
150
+ try {
151
+ const response = await race(config.fetch(url, { ...init, signal }));
152
+ if (!isResponseLike(response)) throw new NotAResponse();
153
+ const text = await race(response.text());
154
+ let parsed;
155
+ try {
156
+ parsed = JSON.parse(text);
157
+ } catch {
158
+ parsed = text;
159
+ }
160
+ received = {
161
+ status: response.status,
162
+ retryAfter: response.headers.get("retry-after"),
163
+ body: parsed
164
+ };
165
+ } catch (cause) {
166
+ if (cause instanceof NotAResponse) {
167
+ throw new TelemError("the injected fetch returned something that is not a Response", {
168
+ code: "connection_error"
169
+ });
170
+ }
171
+ if (abort?.aborted) throw abort.reason ?? cause;
172
+ if (signal?.aborted) {
173
+ throw new TimeoutError(`request to ${url} timed out after ${config.timeoutMs}ms`, cause);
174
+ }
175
+ if (attempt >= config.maxRetries) throw connectionError(url, cause);
176
+ await pause(backoff(attempt));
177
+ continue;
178
+ } finally {
179
+ signal?.removeEventListener("abort", onAbort2);
180
+ }
181
+ if (received.status >= 200 && received.status < 300) return received.body;
182
+ if (isRetryable(received.status) && attempt < config.maxRetries) {
183
+ await pause(retryDelay(received.retryAfter, attempt));
184
+ continue;
185
+ }
186
+ throw statusError(received.status, received.body);
187
+ }
188
+ }
189
+ }
190
+
191
+ // src/trajectory.ts
192
+ var NS_TRAJECTORY = "443866ab-1b45-5ed8-979e-52fdad07b810";
193
+ var NONE = "none";
194
+ var RESERVED_METADATA = /* @__PURE__ */ new Set([
195
+ "message_history",
196
+ "session_key",
197
+ "fingerprint",
198
+ "node_key",
199
+ "kind",
200
+ "parent_node_key",
201
+ "ancestors"
202
+ ]);
203
+ var ENCODER = new TextEncoder();
204
+ var NAMESPACE_BYTES = Uint8Array.from(
205
+ NS_TRAJECTORY.replaceAll("-", "").match(/../g) ?? [],
206
+ (pair) => parseInt(pair, 16)
207
+ );
208
+ function hex(bytes) {
209
+ let out = "";
210
+ for (const byte of bytes) out += byte.toString(16).padStart(2, "0");
211
+ return out;
212
+ }
213
+ async function digest(algorithm, bytes) {
214
+ return new Uint8Array(await crypto.subtle.digest(algorithm, bytes));
215
+ }
216
+ async function sha256(value) {
217
+ return hex(await digest("SHA-256", ENCODER.encode(value)));
218
+ }
219
+ function lp(value) {
220
+ return `${ENCODER.encode(value).length}:${value}`;
221
+ }
222
+ async function uuid5(name) {
223
+ const encoded = ENCODER.encode(name);
224
+ const input = new Uint8Array(NAMESPACE_BYTES.length + encoded.length);
225
+ input.set(NAMESPACE_BYTES);
226
+ input.set(encoded, NAMESPACE_BYTES.length);
227
+ const bytes = (await digest("SHA-1", input)).slice(0, 16);
228
+ bytes[6] = bytes[6] & 15 | 80;
229
+ bytes[8] = bytes[8] & 63 | 128;
230
+ const chars = hex(bytes);
231
+ return [
232
+ chars.slice(0, 8),
233
+ chars.slice(8, 12),
234
+ chars.slice(12, 16),
235
+ chars.slice(16, 20),
236
+ chars.slice(20)
237
+ ].join("-");
238
+ }
239
+ async function fingerprint(harness, conversationId) {
240
+ return await sha256(lp(harness) + lp(conversationId));
241
+ }
242
+ async function sessionKey(harness, conversationId, windowId) {
243
+ const name = lp(harness) + lp(await sha256(conversationId)) + lp(windowId) + lp(NONE);
244
+ return await uuid5(name);
245
+ }
246
+ async function eventNodeKey(harness, session, messageId, toolCallId) {
247
+ const name = lp(harness) + lp(session) + lp(messageId) + lp(toolCallId) + lp("event");
248
+ return await uuid5(name);
249
+ }
250
+ async function snapshotNodeKey(harness, conversationId, messageId) {
251
+ const name = lp(harness) + lp(await sha256(conversationId)) + lp(messageId) + lp("snap");
252
+ return await uuid5(name);
253
+ }
254
+ async function contentAnchor(...parts) {
255
+ return await sha256(parts.map(lp).join(""));
256
+ }
257
+ var HISTORY_TEXT_CAP = 128e3;
258
+ var TOOL_INPUT_CAP = 128e3;
259
+ function cap(value) {
260
+ if (value.length <= HISTORY_TEXT_CAP) return value;
261
+ return Array.from(value).slice(0, HISTORY_TEXT_CAP).join("");
262
+ }
263
+ function toolMarker(name, status, args) {
264
+ let serialized = "";
265
+ try {
266
+ const text = JSON.stringify(args);
267
+ if (typeof text === "string") serialized = text;
268
+ } catch {
269
+ serialized = "";
270
+ }
271
+ if (serialized.length > TOOL_INPUT_CAP) serialized = Array.from(serialized).slice(0, TOOL_INPUT_CAP).join("");
272
+ const suffix = serialized ? ` ${serialized}` : "";
273
+ return `[tool ${name || "?"}: ${status}${suffix}]`;
274
+ }
275
+ function pyStr(value) {
276
+ if (typeof value === "string") return value;
277
+ if (value === null) return "None";
278
+ if (value === true) return "True";
279
+ if (value === false) return "False";
280
+ return String(value);
281
+ }
282
+ function messageText(content) {
283
+ if (typeof content === "string") return cap(content);
284
+ if (Array.isArray(content)) {
285
+ const pieces = [];
286
+ for (const block of content) {
287
+ if (typeof block === "string") pieces.push(block);
288
+ else if (isObject(block) && typeof block.text === "string") pieces.push(block.text);
289
+ }
290
+ return cap(pieces.join("\n"));
291
+ }
292
+ return content === null || content === void 0 ? "" : cap(pyStr(content));
293
+ }
294
+ async function windowAnchor(messages) {
295
+ const first = messages[0];
296
+ if (first === void 0) return NONE;
297
+ return await contentAnchor(
298
+ first.role === void 0 ? "" : pyStr(first.role),
299
+ messageText(first.content)
300
+ );
301
+ }
302
+ async function buildMetadata(args) {
303
+ const [session, fp] = await Promise.all([
304
+ sessionKey(args.harness, args.conversationId, args.windowId),
305
+ fingerprint(args.harness, args.conversationId)
306
+ ]);
307
+ return {
308
+ message_history: args.history,
309
+ session_key: session,
310
+ fingerprint: fp,
311
+ node_key: await eventNodeKey(args.harness, session, args.messageId, args.toolCallId),
312
+ parent_node_key: parentNodeKey(args.ancestors),
313
+ ancestors: args.ancestors
314
+ };
315
+ }
316
+ async function buildSnapshot(args) {
317
+ const [session, fp, node] = await Promise.all([
318
+ sessionKey(args.harness, args.conversationId, args.windowId),
319
+ fingerprint(args.harness, args.conversationId),
320
+ snapshotNodeKey(args.harness, args.conversationId, args.messageId)
321
+ ]);
322
+ return {
323
+ session_key: session,
324
+ fingerprint: fp,
325
+ node_key: node,
326
+ parent_node_key: parentNodeKey(args.ancestors),
327
+ context: args.context,
328
+ spawned_at: args.spawnedAt ?? (/* @__PURE__ */ new Date()).toISOString()
329
+ };
330
+ }
331
+ function parentNodeKey(ancestors) {
332
+ return ancestors.at(-1)?.node_key ?? null;
333
+ }
334
+ var TRAJECTORY = /* @__PURE__ */ Symbol.for("telem.trajectory");
335
+ function withTrajectory(options, envelope) {
336
+ return { ...options, [TRAJECTORY]: envelope };
337
+ }
338
+ function wireMetadata(options, kind) {
339
+ const metadata = {};
340
+ for (const [key, value] of Object.entries(options.metadata ?? {})) {
341
+ if (!RESERVED_METADATA.has(key)) metadata[key] = value;
342
+ }
343
+ const envelope = options[TRAJECTORY];
344
+ if (envelope !== void 0) Object.assign(metadata, envelope, { kind });
345
+ const sessionId = metadata.session_key === void 0 ? options.session : void 0;
346
+ return { metadata, sessionId };
347
+ }
348
+
349
+ export {
350
+ TelemError,
351
+ TimeoutError,
352
+ isObject,
353
+ asObject,
354
+ asArray,
355
+ invalidRequest,
356
+ stringList,
357
+ request,
358
+ RESERVED_METADATA,
359
+ sha256,
360
+ fingerprint,
361
+ sessionKey,
362
+ eventNodeKey,
363
+ snapshotNodeKey,
364
+ contentAnchor,
365
+ cap,
366
+ toolMarker,
367
+ messageText,
368
+ windowAnchor,
369
+ buildMetadata,
370
+ buildSnapshot,
371
+ withTrajectory,
372
+ wireMetadata
373
+ };