@telemai/sdk 0.1.1 → 0.2.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 CHANGED
@@ -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/TelemSDK/tree/main/js-sdk/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
@@ -245,20 +275,30 @@ const windowId = await windowAnchor(messages) // once, from the first message
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
+ };
package/dist/index.cjs CHANGED
@@ -32,6 +32,7 @@ __export(index_exports, {
32
32
  fingerprint: () => fingerprint,
33
33
  sessionKey: () => sessionKey,
34
34
  snapshotNodeKey: () => snapshotNodeKey,
35
+ toolMarker: () => toolMarker,
35
36
  windowAnchor: () => windowAnchor,
36
37
  withTrajectory: () => withTrajectory
37
38
  });
@@ -58,7 +59,7 @@ var TimeoutError = class extends TelemError {
58
59
  };
59
60
 
60
61
  // src/version.ts
61
- var VERSION = "0.1.1";
62
+ var VERSION = "0.2.0";
62
63
 
63
64
  // src/config.ts
64
65
  var DEFAULT_BASE_URL = "https://router.telem.ai";
@@ -243,7 +244,7 @@ function retryDelay(retryAfter, attempt) {
243
244
  }
244
245
  return backoff(attempt);
245
246
  }
246
- async function request(config, method, path, body) {
247
+ async function request(config, method, path, body, abort) {
247
248
  const url = `${config.baseUrl}/${path.replace(/^\/+/, "")}`;
248
249
  const headers = { "User-Agent": config.userAgent };
249
250
  if (config.apiKey) headers.Authorization = `Bearer ${config.apiKey}`;
@@ -256,52 +257,71 @@ async function request(config, method, path, body) {
256
257
  throw invalidRequest(`request body is not JSON: ${cause?.message ?? String(cause)}`);
257
258
  }
258
259
  }
259
- for (let attempt = 0; ; attempt++) {
260
- const signal = Number.isFinite(config.timeoutMs) ? AbortSignal.timeout(Math.min(config.timeoutMs, MAX_TIMEOUT_MS)) : void 0;
261
- let onAbort = () => {
262
- };
263
- const deadline = signal && new Promise((_, reject) => {
264
- onAbort = () => reject(signal.reason);
265
- signal.addEventListener("abort", onAbort, { once: true });
266
- });
267
- const race = (work) => deadline ? Promise.race([work, deadline]) : work;
268
- let received;
269
- try {
270
- const response = await race(config.fetch(url, { ...init, signal }));
271
- if (!isResponseLike(response)) throw new NotAResponse();
272
- const text = await race(response.text());
273
- let parsed;
274
- try {
275
- parsed = JSON.parse(text);
276
- } catch {
277
- parsed = text;
278
- }
279
- received = {
280
- status: response.status,
281
- retryAfter: response.headers.get("retry-after"),
282
- body: parsed
260
+ let onAbort = () => {
261
+ };
262
+ const abortion = abort && new Promise((_, reject) => {
263
+ onAbort = () => reject(abort.reason ?? new Error("aborted"));
264
+ abort.addEventListener("abort", onAbort, { once: true });
265
+ });
266
+ abortion?.catch(() => {
267
+ });
268
+ const pause = (ms) => abortion ? Promise.race([config.sleep(ms), abortion]) : config.sleep(ms);
269
+ try {
270
+ return await attempts();
271
+ } finally {
272
+ abort?.removeEventListener("abort", onAbort);
273
+ }
274
+ async function attempts() {
275
+ for (let attempt = 0; ; attempt++) {
276
+ if (abort?.aborted) throw abort.reason ?? new Error("aborted");
277
+ const timeout = Number.isFinite(config.timeoutMs) ? AbortSignal.timeout(Math.min(config.timeoutMs, MAX_TIMEOUT_MS)) : void 0;
278
+ const signal = timeout && abort ? typeof AbortSignal.any === "function" ? AbortSignal.any([timeout, abort]) : timeout : timeout ?? abort;
279
+ let onAbort2 = () => {
283
280
  };
284
- } catch (cause) {
285
- if (cause instanceof NotAResponse) {
286
- throw new TelemError("the injected fetch returned something that is not a Response", {
287
- code: "connection_error"
288
- });
281
+ const deadline = signal && new Promise((_, reject) => {
282
+ onAbort2 = () => reject(signal.reason);
283
+ signal.addEventListener("abort", onAbort2, { once: true });
284
+ });
285
+ const race = (work) => deadline ? Promise.race([work, deadline]) : work;
286
+ let received;
287
+ try {
288
+ const response = await race(config.fetch(url, { ...init, signal }));
289
+ if (!isResponseLike(response)) throw new NotAResponse();
290
+ const text = await race(response.text());
291
+ let parsed;
292
+ try {
293
+ parsed = JSON.parse(text);
294
+ } catch {
295
+ parsed = text;
296
+ }
297
+ received = {
298
+ status: response.status,
299
+ retryAfter: response.headers.get("retry-after"),
300
+ body: parsed
301
+ };
302
+ } catch (cause) {
303
+ if (cause instanceof NotAResponse) {
304
+ throw new TelemError("the injected fetch returned something that is not a Response", {
305
+ code: "connection_error"
306
+ });
307
+ }
308
+ if (abort?.aborted) throw abort.reason ?? cause;
309
+ if (signal?.aborted) {
310
+ throw new TimeoutError(`request to ${url} timed out after ${config.timeoutMs}ms`, cause);
311
+ }
312
+ if (attempt >= config.maxRetries) throw connectionError(url, cause);
313
+ await pause(backoff(attempt));
314
+ continue;
315
+ } finally {
316
+ signal?.removeEventListener("abort", onAbort2);
289
317
  }
290
- if (signal?.aborted) {
291
- throw new TimeoutError(`request to ${url} timed out after ${config.timeoutMs}ms`, cause);
318
+ if (received.status >= 200 && received.status < 300) return received.body;
319
+ if (isRetryable(received.status) && attempt < config.maxRetries) {
320
+ await pause(retryDelay(received.retryAfter, attempt));
321
+ continue;
292
322
  }
293
- if (attempt >= config.maxRetries) throw connectionError(url, cause);
294
- await config.sleep(backoff(attempt));
295
- continue;
296
- } finally {
297
- signal?.removeEventListener("abort", onAbort);
323
+ throw statusError(received.status, received.body);
298
324
  }
299
- if (received.status >= 200 && received.status < 300) return received.body;
300
- if (isRetryable(received.status) && attempt < config.maxRetries) {
301
- await config.sleep(retryDelay(received.retryAfter, attempt));
302
- continue;
303
- }
304
- throw statusError(received.status, received.body);
305
325
  }
306
326
  }
307
327
 
@@ -372,10 +392,23 @@ async function contentAnchor(...parts) {
372
392
  return await sha256(parts.map(lp).join(""));
373
393
  }
374
394
  var HISTORY_TEXT_CAP = 128e3;
395
+ var TOOL_INPUT_CAP = 128e3;
375
396
  function cap(value) {
376
397
  if (value.length <= HISTORY_TEXT_CAP) return value;
377
398
  return Array.from(value).slice(0, HISTORY_TEXT_CAP).join("");
378
399
  }
400
+ function toolMarker(name, status, args) {
401
+ let serialized = "";
402
+ try {
403
+ const text = JSON.stringify(args);
404
+ if (typeof text === "string") serialized = text;
405
+ } catch {
406
+ serialized = "";
407
+ }
408
+ if (serialized.length > TOOL_INPUT_CAP) serialized = Array.from(serialized).slice(0, TOOL_INPUT_CAP).join("");
409
+ const suffix = serialized ? ` ${serialized}` : "";
410
+ return `[tool ${name || "?"}: ${status}${suffix}]`;
411
+ }
379
412
  function pyStr(value) {
380
413
  if (typeof value === "string") return value;
381
414
  if (value === null) return "None";
@@ -505,7 +538,7 @@ function buildFetchResponse(interaction) {
505
538
  }
506
539
  async function fetchUrls(config, url, options) {
507
540
  const body = fetchBody(url, options);
508
- return buildFetchResponse(await request(config, "POST", "/v1/fetch", body));
541
+ return buildFetchResponse(await request(config, "POST", "/v1/fetch", body, options.signal));
509
542
  }
510
543
 
511
544
  // src/methods/providers.ts
@@ -654,7 +687,7 @@ function buildSearchResponse(interaction) {
654
687
  }
655
688
  async function search(config, query, options) {
656
689
  const body = searchBody(config, query, options);
657
- const data = await request(config, "POST", "/v1/interactions", body);
690
+ const data = await request(config, "POST", "/v1/interactions", body, options.signal);
658
691
  assertV2(data);
659
692
  return buildSearchResponse(data);
660
693
  }
@@ -783,6 +816,7 @@ var index_default = Telem;
783
816
  fingerprint,
784
817
  sessionKey,
785
818
  snapshotNodeKey,
819
+ toolMarker,
786
820
  windowAnchor,
787
821
  withTrajectory
788
822
  });
package/dist/index.d.cts CHANGED
@@ -70,6 +70,9 @@ type SearchOptions = {
70
70
  goal?: string;
71
71
  context?: string;
72
72
  session?: string;
73
+ /** Abort this call: the request stops, no retry follows, and the call rejects
74
+ * with the signal's reason. Merged with the per-attempt timeout. */
75
+ signal?: AbortSignal;
73
76
  /** Freeform metadata for the wire. Reserved trajectory-lineage keys
74
77
  * (`RESERVED_METADATA`) are STRIPPED from here — lineage may only be attached
75
78
  * through `withTrajectory`, fed by `buildMetadata`, never smuggled in as data. */
@@ -84,6 +87,9 @@ type FetchOptions = {
84
87
  inlineMaxChars?: number;
85
88
  contentFormat?: string;
86
89
  session?: string;
90
+ /** Abort this call: the request stops, no retry follows, and the call rejects
91
+ * with the signal's reason. Merged with the per-attempt timeout. */
92
+ signal?: AbortSignal;
87
93
  metadata?: Json;
88
94
  };
89
95
  type TelemOptions = SearchDefaults & {
@@ -258,6 +264,12 @@ declare function snapshotNodeKey(harness: string, conversationId: string, messag
258
264
  /** Stable anchor id for content a host did not assign an id to.
259
265
  * Length-prefixed like every other key component, so concatenation stays injective. */
260
266
  declare function contentAnchor(...parts: string[]): Promise<string>;
267
+ /** `tool_marker` from telem/integrations/_trajectory_v5.py: one tool call as the
268
+ * compact acting-trace marker every harness renders into history. The exporter
269
+ * cuts goals on `[tool telem_search:`, so the text is a contract. Compact JSON,
270
+ * key order kept, matches Python's `separators=(",", ":")`; an unserializable
271
+ * input drops the suffix rather than the marker. */
272
+ declare function toolMarker(name: string, status: string, args: unknown): string;
261
273
  /**
262
274
  * Identify this context-window generation by its first message.
263
275
  *
@@ -366,4 +378,4 @@ declare class Telem {
366
378
  providers(): Promise<ProviderInfo[]>;
367
379
  }
368
380
 
369
- export { type FetchOptions, type FetchResponse, type FetchResult, type Json, type ProviderInfo, type ProviderRun, RESERVED_METADATA, type SearchDefaults, type SearchOptions, type SearchResponse, type SearchResult, type SessionResults, type SessionSummary, type SessionsResource, type SnapshotArgs, Telem, type TelemClientErrorCode, TelemError, type TelemErrorOptions, type TelemOptions, TimeoutError, type TrajectoryArgs, type TrajectoryEnvelope, type TrajectorySnapshot, buildMetadata, buildSnapshot, contentAnchor, Telem as default, eventNodeKey, fingerprint, sessionKey, snapshotNodeKey, windowAnchor, withTrajectory };
381
+ export { type FetchOptions, type FetchResponse, type FetchResult, type Json, type ProviderInfo, type ProviderRun, RESERVED_METADATA, type SearchDefaults, type SearchOptions, type SearchResponse, type SearchResult, type SessionResults, type SessionSummary, type SessionsResource, type SnapshotArgs, Telem, type TelemClientErrorCode, TelemError, type TelemErrorOptions, type TelemOptions, TimeoutError, type TrajectoryArgs, type TrajectoryEnvelope, type TrajectorySnapshot, buildMetadata, buildSnapshot, contentAnchor, Telem as default, eventNodeKey, fingerprint, sessionKey, snapshotNodeKey, toolMarker, windowAnchor, withTrajectory };