@mtharrison/loupe 1.1.1 → 1.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type ChatModelLike, type LocalLLMTracer, type TraceConfig, type TraceContext, type TraceRequest } from './types';
2
- export type { ChatModelLike, HierarchyNode, HierarchyResponse, LocalLLMTracer, NormalizedTraceContext, TraceConfig, TraceContext, TraceEvent, TraceFilters, TraceHierarchy, TraceListResponse, TraceMode, TraceRecord, TraceRequest, TraceServer, TraceStatus, TraceSummary, TraceTags, UIReloadEvent, } from './types';
1
+ import { type ChatModelLike, type LocalLLMTracer, type OpenAIChatCompletionCreateParamsLike, type OpenAIClientLike, type TraceConfig, type TraceContext, type TraceRequest } from './types';
2
+ export type { ChatModelLike, HierarchyNode, HierarchyResponse, LocalLLMTracer, NormalizedTraceContext, OpenAIChatCompletionCreateParamsLike, OpenAIChatCompletionStreamLike, OpenAIClientLike, TraceConfig, TraceContext, TraceEvent, TraceFilters, TraceHierarchy, TraceListResponse, TraceMode, TraceRecord, TraceRequest, TraceServer, TraceStatus, TraceSummary, TraceTags, UIReloadEvent, } from './types';
3
3
  export declare function isTraceEnabled(): boolean;
4
4
  export declare function getLocalLLMTracer(config?: TraceConfig): LocalLLMTracer;
5
5
  export declare function startTraceServer(config?: TraceConfig): Promise<{
@@ -15,3 +15,4 @@ export declare function recordStreamFinish(traceId: string, chunk: unknown, conf
15
15
  export declare function recordError(traceId: string, error: unknown, config?: TraceConfig): void;
16
16
  export declare function __resetLocalLLMTracerForTests(): void;
17
17
  export declare function wrapChatModel<TModel extends ChatModelLike<TInput, TOptions, TValue, TChunk>, TInput = any, TOptions = any, TValue = any, TChunk = any>(model: TModel, getContext: () => TraceContext, config?: TraceConfig): TModel;
18
+ export declare function wrapOpenAIClient<TClient extends OpenAIClientLike<TParams, TOptions, TResponse, TChunk>, TParams extends OpenAIChatCompletionCreateParamsLike = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any>(client: TClient, getContext: () => TraceContext, config?: TraceConfig): TClient;
package/dist/index.js CHANGED
@@ -11,11 +11,13 @@ exports.recordStreamFinish = recordStreamFinish;
11
11
  exports.recordError = recordError;
12
12
  exports.__resetLocalLLMTracerForTests = __resetLocalLLMTracerForTests;
13
13
  exports.wrapChatModel = wrapChatModel;
14
+ exports.wrapOpenAIClient = wrapOpenAIClient;
14
15
  const server_1 = require("./server");
15
16
  const store_1 = require("./store");
16
17
  const ui_build_1 = require("./ui-build");
17
18
  const utils_1 = require("./utils");
18
19
  let singleton = null;
20
+ const DEFAULT_TRACE_PORT = 4319;
19
21
  function isTraceEnabled() {
20
22
  return (0, utils_1.envFlag)('LLM_TRACE_ENABLED');
21
23
  }
@@ -105,9 +107,66 @@ function wrapChatModel(model, getContext, config) {
105
107
  },
106
108
  };
107
109
  }
110
+ function wrapOpenAIClient(client, getContext, config) {
111
+ if (!client || typeof client.chat?.completions?.create !== 'function') {
112
+ throw new TypeError('wrapOpenAIClient expects an OpenAI client with chat.completions.create().');
113
+ }
114
+ const wrappedCompletions = new Proxy(client.chat.completions, {
115
+ get(target, prop, receiver) {
116
+ if (prop === 'create') {
117
+ return async (params, options) => {
118
+ const tracer = getLocalLLMTracer(config);
119
+ if (!tracer.isEnabled()) {
120
+ return target.create.call(target, params, options);
121
+ }
122
+ const context = withOpenAITraceContext(getContext ? getContext() : {}, params);
123
+ if (params?.stream) {
124
+ const traceId = tracer.recordStreamStart(context, { input: params, options: options });
125
+ try {
126
+ const stream = await target.create.call(target, params, options);
127
+ return wrapOpenAIChatCompletionsStream(stream, tracer, traceId);
128
+ }
129
+ catch (error) {
130
+ tracer.recordError(traceId, error);
131
+ throw error;
132
+ }
133
+ }
134
+ const traceId = tracer.recordInvokeStart(context, { input: params, options: options });
135
+ try {
136
+ const response = await target.create.call(target, params, options);
137
+ tracer.recordInvokeFinish(traceId, normalizeOpenAIChatCompletionResponse(response));
138
+ return response;
139
+ }
140
+ catch (error) {
141
+ tracer.recordError(traceId, error);
142
+ throw error;
143
+ }
144
+ };
145
+ }
146
+ return bindMethod(target, Reflect.get(target, prop, receiver));
147
+ },
148
+ });
149
+ const wrappedChat = new Proxy(client.chat, {
150
+ get(target, prop, receiver) {
151
+ if (prop === 'completions') {
152
+ return wrappedCompletions;
153
+ }
154
+ return bindMethod(target, Reflect.get(target, prop, receiver));
155
+ },
156
+ });
157
+ return new Proxy(client, {
158
+ get(target, prop, receiver) {
159
+ if (prop === 'chat') {
160
+ return wrappedChat;
161
+ }
162
+ return bindMethod(target, Reflect.get(target, prop, receiver));
163
+ },
164
+ });
165
+ }
108
166
  class LocalLLMTracerImpl {
109
167
  config;
110
168
  loggedUrl;
169
+ portWasExplicit;
111
170
  server;
112
171
  serverFailed;
113
172
  serverInfo;
@@ -121,6 +180,7 @@ class LocalLLMTracerImpl {
121
180
  port: 4319,
122
181
  uiHotReload: false,
123
182
  };
183
+ this.portWasExplicit = false;
124
184
  this.configure(config);
125
185
  this.store = new store_1.TraceStore({ maxTraces: this.config.maxTraces });
126
186
  this.server = null;
@@ -134,9 +194,11 @@ class LocalLLMTracerImpl {
134
194
  if (this.serverInfo && (config.host || config.port)) {
135
195
  return;
136
196
  }
197
+ const explicitPort = getConfiguredPort(config.port, process.env.LLM_TRACE_PORT, this.portWasExplicit ? this.config.port : undefined);
198
+ this.portWasExplicit = explicitPort !== undefined;
137
199
  this.config = {
138
200
  host: config.host || this.config.host || process.env.LLM_TRACE_HOST || '127.0.0.1',
139
- port: Number(config.port || this.config.port || process.env.LLM_TRACE_PORT) || 4319,
201
+ port: explicitPort ?? DEFAULT_TRACE_PORT,
140
202
  maxTraces: Number(config.maxTraces || this.config.maxTraces || process.env.LLM_TRACE_MAX_TRACES) || 1000,
141
203
  uiHotReload: typeof config.uiHotReload === 'boolean'
142
204
  ? config.uiHotReload
@@ -163,7 +225,10 @@ class LocalLLMTracerImpl {
163
225
  }
164
226
  this.serverStartPromise = (async () => {
165
227
  try {
166
- this.server = (0, server_1.createTraceServer)(this.store, this.config);
228
+ this.server = (0, server_1.createTraceServer)(this.store, {
229
+ ...this.config,
230
+ allowPortFallback: !this.portWasExplicit,
231
+ });
167
232
  this.serverInfo = await this.server.start();
168
233
  if (this.serverInfo && !this.uiWatcher) {
169
234
  this.uiWatcher = await (0, ui_build_1.maybeStartUIWatcher)(() => {
@@ -218,3 +283,266 @@ function normaliseRequest(request) {
218
283
  options: (0, utils_1.safeClone)(request?.options),
219
284
  };
220
285
  }
286
+ function bindMethod(target, value) {
287
+ return typeof value === 'function' ? value.bind(target) : value;
288
+ }
289
+ function getConfiguredPort(configPort, envPort, currentExplicitPort) {
290
+ if (typeof configPort === 'number' && Number.isFinite(configPort)) {
291
+ return configPort;
292
+ }
293
+ if (envPort !== undefined) {
294
+ const parsed = Number(envPort);
295
+ if (Number.isFinite(parsed)) {
296
+ return parsed;
297
+ }
298
+ }
299
+ return typeof currentExplicitPort === 'number' && Number.isFinite(currentExplicitPort) ? currentExplicitPort : undefined;
300
+ }
301
+ function withOpenAITraceContext(context, params) {
302
+ return {
303
+ ...(context || {}),
304
+ model: context?.model || (typeof params?.model === 'string' ? params.model : null),
305
+ provider: context?.provider || 'openai',
306
+ };
307
+ }
308
+ function wrapOpenAIChatCompletionsStream(stream, tracer, traceId) {
309
+ const state = {
310
+ content: '',
311
+ finished: false,
312
+ finishReasons: [],
313
+ began: false,
314
+ role: null,
315
+ toolCalls: new Map(),
316
+ usage: null,
317
+ };
318
+ const emitBegin = (role) => {
319
+ if (state.began) {
320
+ return;
321
+ }
322
+ state.began = true;
323
+ const nextRole = role || state.role || 'assistant';
324
+ state.role = nextRole;
325
+ tracer.recordStreamChunk(traceId, { type: 'begin', role: nextRole });
326
+ };
327
+ const emitFinish = () => {
328
+ if (state.finished) {
329
+ return;
330
+ }
331
+ emitBegin(state.role);
332
+ state.finished = true;
333
+ tracer.recordStreamFinish(traceId, {
334
+ type: 'finish',
335
+ finish_reasons: state.finishReasons,
336
+ message: {
337
+ role: state.role || 'assistant',
338
+ content: state.content || null,
339
+ },
340
+ tool_calls: serializeOpenAIToolCalls(state.toolCalls),
341
+ usage: (0, utils_1.safeClone)(state.usage),
342
+ });
343
+ };
344
+ const processChunk = (chunk) => {
345
+ const raw = (0, utils_1.safeClone)(chunk);
346
+ const chunkToolCalls = [];
347
+ const finishReasons = new Set();
348
+ const contentParts = [];
349
+ const choices = Array.isArray(chunk?.choices) ? chunk.choices : [];
350
+ let sawEvent = false;
351
+ for (const choice of choices) {
352
+ const delta = choice?.delta || {};
353
+ if (typeof delta?.role === 'string' && delta.role) {
354
+ state.role = delta.role;
355
+ sawEvent = true;
356
+ }
357
+ for (const part of extractOpenAITextParts(delta?.content)) {
358
+ state.content = `${state.content}${part}`;
359
+ contentParts.push(part);
360
+ sawEvent = true;
361
+ }
362
+ if (Array.isArray(delta?.tool_calls) && delta.tool_calls.length > 0) {
363
+ sawEvent = true;
364
+ for (const toolCall of delta.tool_calls) {
365
+ mergeOpenAIToolCallDelta(state.toolCalls, toolCall);
366
+ chunkToolCalls.push((0, utils_1.safeClone)(toolCall));
367
+ }
368
+ }
369
+ if (typeof choice?.finish_reason === 'string' && choice.finish_reason) {
370
+ finishReasons.add(choice.finish_reason);
371
+ }
372
+ }
373
+ const usage = normalizeOpenAIUsage(chunk?.usage);
374
+ if (usage) {
375
+ state.usage = usage;
376
+ sawEvent = true;
377
+ }
378
+ for (const finishReason of finishReasons) {
379
+ state.finishReasons.push(finishReason);
380
+ }
381
+ if (sawEvent) {
382
+ emitBegin(state.role);
383
+ }
384
+ if (contentParts.length > 0) {
385
+ tracer.recordStreamChunk(traceId, {
386
+ type: 'chunk',
387
+ content: contentParts.join(''),
388
+ finish_reasons: [...finishReasons],
389
+ raw,
390
+ tool_calls: chunkToolCalls,
391
+ usage,
392
+ });
393
+ return;
394
+ }
395
+ tracer.recordStreamChunk(traceId, {
396
+ type: 'event',
397
+ finish_reasons: [...finishReasons],
398
+ raw,
399
+ tool_calls: chunkToolCalls,
400
+ usage,
401
+ });
402
+ };
403
+ const createWrappedIterator = (iterator) => ({
404
+ async next(...args) {
405
+ try {
406
+ const result = await iterator.next(...args);
407
+ if (result.done) {
408
+ emitFinish();
409
+ return result;
410
+ }
411
+ processChunk(result.value);
412
+ return result;
413
+ }
414
+ catch (error) {
415
+ tracer.recordError(traceId, error);
416
+ throw error;
417
+ }
418
+ },
419
+ async return(value) {
420
+ try {
421
+ const result = typeof iterator.return === 'function'
422
+ ? await iterator.return(value)
423
+ : {
424
+ done: true,
425
+ value,
426
+ };
427
+ emitFinish();
428
+ return result;
429
+ }
430
+ catch (error) {
431
+ tracer.recordError(traceId, error);
432
+ throw error;
433
+ }
434
+ },
435
+ async throw(error) {
436
+ tracer.recordError(traceId, error);
437
+ if (typeof iterator.throw === 'function') {
438
+ return iterator.throw(error);
439
+ }
440
+ throw error;
441
+ },
442
+ [Symbol.asyncIterator]() {
443
+ return this;
444
+ },
445
+ });
446
+ return new Proxy(stream, {
447
+ get(target, prop, receiver) {
448
+ if (prop === Symbol.asyncIterator) {
449
+ return () => createWrappedIterator(target[Symbol.asyncIterator]());
450
+ }
451
+ return bindMethod(target, Reflect.get(target, prop, receiver));
452
+ },
453
+ });
454
+ }
455
+ function normalizeOpenAIChatCompletionResponse(response) {
456
+ const message = response?.choices?.[0]?.message;
457
+ return {
458
+ finish_reason: response?.choices?.[0]?.finish_reason || null,
459
+ id: response?.id || null,
460
+ message: normalizeOpenAIMessage(message),
461
+ model: response?.model || null,
462
+ object: response?.object || null,
463
+ raw: (0, utils_1.safeClone)(response),
464
+ tool_calls: Array.isArray(message?.tool_calls) ? (0, utils_1.safeClone)(message.tool_calls) : [],
465
+ usage: normalizeOpenAIUsage(response?.usage),
466
+ };
467
+ }
468
+ function normalizeOpenAIMessage(message) {
469
+ return {
470
+ ...(0, utils_1.safeClone)(message),
471
+ content: normalizeOpenAIMessageContent(message),
472
+ role: typeof message?.role === 'string' ? message.role : 'assistant',
473
+ };
474
+ }
475
+ function normalizeOpenAIMessageContent(message) {
476
+ const content = extractOpenAITextParts(message?.content);
477
+ if (content.length > 0) {
478
+ return content.join('');
479
+ }
480
+ if (typeof message?.refusal === 'string' && message.refusal) {
481
+ return message.refusal;
482
+ }
483
+ return message?.content ?? null;
484
+ }
485
+ function extractOpenAITextParts(content) {
486
+ if (typeof content === 'string' && content) {
487
+ return [content];
488
+ }
489
+ if (!Array.isArray(content)) {
490
+ return [];
491
+ }
492
+ const parts = [];
493
+ for (const item of content) {
494
+ if (typeof item === 'string' && item) {
495
+ parts.push(item);
496
+ continue;
497
+ }
498
+ if (typeof item?.text === 'string' && item.text) {
499
+ parts.push(item.text);
500
+ continue;
501
+ }
502
+ if (typeof item?.content === 'string' && item.content) {
503
+ parts.push(item.content);
504
+ }
505
+ }
506
+ return parts;
507
+ }
508
+ function normalizeOpenAIUsage(usage) {
509
+ if (!usage || typeof usage !== 'object') {
510
+ return null;
511
+ }
512
+ return {
513
+ raw: (0, utils_1.safeClone)(usage),
514
+ tokens: {
515
+ completion: typeof usage?.completion_tokens === 'number' ? usage.completion_tokens : null,
516
+ prompt: typeof usage?.prompt_tokens === 'number' ? usage.prompt_tokens : null,
517
+ total: typeof usage?.total_tokens === 'number' ? usage.total_tokens : null,
518
+ },
519
+ };
520
+ }
521
+ function mergeOpenAIToolCallDelta(target, delta) {
522
+ const index = Number.isInteger(delta?.index) ? delta.index : target.size;
523
+ const current = (0, utils_1.safeClone)(target.get(index) || { function: { arguments: '' } });
524
+ if (delta?.id) {
525
+ current.id = delta.id;
526
+ }
527
+ if (delta?.type) {
528
+ current.type = delta.type;
529
+ }
530
+ if (delta?.function?.name) {
531
+ current.function = {
532
+ ...(current.function || {}),
533
+ name: delta.function.name,
534
+ };
535
+ }
536
+ if (delta?.function?.arguments) {
537
+ current.function = {
538
+ ...(current.function || {}),
539
+ arguments: `${current.function?.arguments || ''}${delta.function.arguments}`,
540
+ };
541
+ }
542
+ target.set(index, current);
543
+ }
544
+ function serializeOpenAIToolCalls(toolCalls) {
545
+ return [...toolCalls.entries()]
546
+ .sort(([left], [right]) => left - right)
547
+ .map(([, value]) => (0, utils_1.safeClone)(value));
548
+ }
package/dist/server.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { type TraceServer } from './types';
2
2
  import { TraceStore } from './store';
3
3
  export declare function createTraceServer(store: TraceStore, options?: {
4
+ allowPortFallback?: boolean;
4
5
  host?: string;
5
6
  port?: number;
6
7
  }): TraceServer;
package/dist/server.js CHANGED
@@ -11,11 +11,12 @@ const node_url_1 = require("node:url");
11
11
  const ui_1 = require("./ui");
12
12
  function createTraceServer(store, options = {}) {
13
13
  const host = options.host || '127.0.0.1';
14
- const port = Number(options.port) || 4319;
14
+ const requestedPort = toPortNumber(options.port, 4319);
15
+ let activePort = requestedPort;
15
16
  const clients = new Set();
16
17
  const server = node_http_1.default.createServer(async (req, res) => {
17
18
  try {
18
- const url = new node_url_1.URL(req.url || '/', `http://${req.headers.host || `${host}:${port}`}`);
19
+ const url = new node_url_1.URL(req.url || '/', `http://${req.headers.host || `${host}:${activePort}`}`);
19
20
  if (req.method === 'GET' && url.pathname === '/') {
20
21
  sendHtml(res, (0, ui_1.renderAppHtml)());
21
22
  return;
@@ -62,18 +63,13 @@ function createTraceServer(store, options = {}) {
62
63
  });
63
64
  return {
64
65
  async start() {
65
- await new Promise((resolve, reject) => {
66
- server.once('error', reject);
67
- server.listen(port, host, () => {
68
- server.removeListener('error', reject);
69
- resolve();
70
- });
71
- });
66
+ await listenWithFallback(server, host, requestedPort, options.allowPortFallback);
67
+ activePort = getBoundPort(server, requestedPort);
72
68
  server.unref();
73
69
  return {
74
70
  host,
75
- port,
76
- url: `http://${host}:${port}`,
71
+ port: activePort,
72
+ url: `http://${host}:${activePort}`,
77
73
  };
78
74
  },
79
75
  broadcast,
@@ -92,6 +88,41 @@ function createTraceServer(store, options = {}) {
92
88
  }
93
89
  }
94
90
  }
91
+ async function listenWithFallback(server, host, port, allowPortFallback = false) {
92
+ await new Promise((resolve, reject) => {
93
+ const tryListen = (nextPort, canFallback) => {
94
+ const onError = (error) => {
95
+ server.removeListener('listening', onListening);
96
+ if (canFallback && error?.code === 'EADDRINUSE') {
97
+ tryListen(0, false);
98
+ return;
99
+ }
100
+ reject(error);
101
+ };
102
+ const onListening = () => {
103
+ server.removeListener('error', onError);
104
+ resolve();
105
+ };
106
+ server.once('error', onError);
107
+ server.once('listening', onListening);
108
+ server.listen(nextPort, host);
109
+ };
110
+ tryListen(port, allowPortFallback);
111
+ });
112
+ }
113
+ function getBoundPort(server, fallbackPort) {
114
+ const address = server.address();
115
+ if (address && typeof address === 'object' && typeof address.port === 'number') {
116
+ return address.port;
117
+ }
118
+ return fallbackPort;
119
+ }
120
+ function toPortNumber(value, fallback) {
121
+ if (typeof value === 'number' && Number.isFinite(value)) {
122
+ return value;
123
+ }
124
+ return fallback;
125
+ }
95
126
  function parseFilters(url) {
96
127
  return {
97
128
  search: url.searchParams.get('search') || undefined,
@@ -31,4 +31,14 @@ export type SessionNavItem = {
31
31
  shortSessionId: string;
32
32
  status: "error" | "ok" | "pending";
33
33
  };
34
+ export type SessionTreeSelection = {
35
+ selectedNodeId: string | null;
36
+ selectedTraceId: string | null;
37
+ };
34
38
  export declare function deriveSessionNavItems(sessionNodes: SessionNavHierarchyNode[], traceById: Map<string, SessionNavTraceSummary>): SessionNavItem[];
39
+ export declare function sortSessionNodesForNav(sessionNodes: SessionNavHierarchyNode[], traceById: Map<string, SessionNavTraceSummary>): SessionNavHierarchyNode[];
40
+ export declare function findSessionNodePath(nodes: SessionNavHierarchyNode[], id: string, trail?: SessionNavHierarchyNode[]): SessionNavHierarchyNode[];
41
+ export declare function findSessionNodeById(nodes: SessionNavHierarchyNode[], id: string): SessionNavHierarchyNode | null;
42
+ export declare function getNewestTraceIdForNode(node: SessionNavHierarchyNode | null | undefined): string | null;
43
+ export declare function resolveSessionTreeSelection(sessionNodes: SessionNavHierarchyNode[], selectedNodeId: string | null, selectedTraceId: string | null): SessionTreeSelection;
44
+ export declare function getDefaultExpandedSessionTreeNodeIds(sessionNodes: SessionNavHierarchyNode[], activeSessionId: string | null, selectedNodeId: string | null): Set<string>;
@@ -1,11 +1,96 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deriveSessionNavItems = deriveSessionNavItems;
4
+ exports.sortSessionNodesForNav = sortSessionNodesForNav;
5
+ exports.findSessionNodePath = findSessionNodePath;
6
+ exports.findSessionNodeById = findSessionNodeById;
7
+ exports.getNewestTraceIdForNode = getNewestTraceIdForNode;
8
+ exports.resolveSessionTreeSelection = resolveSessionTreeSelection;
9
+ exports.getDefaultExpandedSessionTreeNodeIds = getDefaultExpandedSessionTreeNodeIds;
4
10
  function deriveSessionNavItems(sessionNodes, traceById) {
5
11
  return sessionNodes
6
12
  .map((node) => deriveSessionNavItem(node, traceById))
7
13
  .sort(compareSessionNavItems);
8
14
  }
15
+ function sortSessionNodesForNav(sessionNodes, traceById) {
16
+ const itemById = new Map(sessionNodes.map((node) => [node.id, deriveSessionNavItem(node, traceById)]));
17
+ return sessionNodes
18
+ .slice()
19
+ .sort((left, right) => compareSessionNavItems(itemById.get(left.id), itemById.get(right.id)));
20
+ }
21
+ function findSessionNodePath(nodes, id, trail = []) {
22
+ for (const node of nodes) {
23
+ const nextTrail = [...trail, node];
24
+ if (node.id === id) {
25
+ return nextTrail;
26
+ }
27
+ const childTrail = findSessionNodePath(node.children, id, nextTrail);
28
+ if (childTrail.length) {
29
+ return childTrail;
30
+ }
31
+ }
32
+ return [];
33
+ }
34
+ function findSessionNodeById(nodes, id) {
35
+ return findSessionNodePath(nodes, id).at(-1) ?? null;
36
+ }
37
+ function getNewestTraceIdForNode(node) {
38
+ if (!node?.traceIds.length) {
39
+ return null;
40
+ }
41
+ if (typeof node.meta?.traceId === "string" && node.meta.traceId) {
42
+ return node.meta.traceId;
43
+ }
44
+ return node.traceIds[0] || null;
45
+ }
46
+ function resolveSessionTreeSelection(sessionNodes, selectedNodeId, selectedTraceId) {
47
+ const selectedNode = selectedNodeId
48
+ ? findSessionNodeById(sessionNodes, selectedNodeId)
49
+ : null;
50
+ const selectedTraceNode = selectedTraceId
51
+ ? findSessionNodeById(sessionNodes, `trace:${selectedTraceId}`)
52
+ : null;
53
+ const fallbackNode = selectedNode ?? selectedTraceNode ?? sessionNodes[0] ?? null;
54
+ if (!fallbackNode) {
55
+ return {
56
+ selectedNodeId: null,
57
+ selectedTraceId: null,
58
+ };
59
+ }
60
+ const nextSelectedNodeId = selectedNode?.id ?? fallbackNode.id;
61
+ const nextSelectedTraceId = selectedTraceId && fallbackNode.traceIds.includes(selectedTraceId)
62
+ ? selectedTraceId
63
+ : getNewestTraceIdForNode(fallbackNode);
64
+ return {
65
+ selectedNodeId: nextSelectedNodeId,
66
+ selectedTraceId: nextSelectedTraceId,
67
+ };
68
+ }
69
+ function getDefaultExpandedSessionTreeNodeIds(sessionNodes, activeSessionId, selectedNodeId) {
70
+ const expanded = new Set();
71
+ const activeSession = (activeSessionId
72
+ ? sessionNodes.find((node) => node.id === activeSessionId) ?? null
73
+ : null) ?? sessionNodes[0] ?? null;
74
+ if (!activeSession) {
75
+ return expanded;
76
+ }
77
+ if (activeSession.children.length) {
78
+ expanded.add(activeSession.id);
79
+ }
80
+ visitSessionTree(activeSession.children, (node) => {
81
+ if (node.children.length && node.type === "actor") {
82
+ expanded.add(node.id);
83
+ }
84
+ });
85
+ if (selectedNodeId) {
86
+ for (const node of findSessionNodePath([activeSession], selectedNodeId)) {
87
+ if (node.children.length) {
88
+ expanded.add(node.id);
89
+ }
90
+ }
91
+ }
92
+ return expanded;
93
+ }
9
94
  function deriveSessionNavItem(node, traceById) {
10
95
  const traces = node.traceIds
11
96
  .map((traceId) => traceById.get(traceId))
@@ -130,3 +215,9 @@ function formatCompactTimestamp(value) {
130
215
  minute: "2-digit",
131
216
  });
132
217
  }
218
+ function visitSessionTree(nodes, visitor) {
219
+ for (const node of nodes) {
220
+ visitor(node);
221
+ visitSessionTree(node.children, visitor);
222
+ }
223
+ }
package/dist/types.d.ts CHANGED
@@ -202,6 +202,24 @@ export interface ChatModelLike<TInput = any, TOptions = any, TValue = any, TChun
202
202
  invoke(input: TInput, options?: TOptions): Promise<TValue>;
203
203
  stream(input: TInput, options?: TOptions): AsyncGenerator<TChunk>;
204
204
  }
205
+ export type OpenAIChatCompletionCreateParamsLike = Record<string, any> & {
206
+ messages?: Record<string, any>[];
207
+ model?: string | null;
208
+ stream?: boolean | null;
209
+ };
210
+ export interface OpenAIChatCompletionStreamLike<TChunk = any> extends AsyncIterable<TChunk> {
211
+ [Symbol.asyncIterator](): AsyncIterator<TChunk>;
212
+ }
213
+ export interface OpenAIChatCompletionsLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
214
+ create(params: TParams, options?: TOptions): Promise<TResponse> | Promise<OpenAIChatCompletionStreamLike<TChunk>> | OpenAIChatCompletionStreamLike<TChunk>;
215
+ }
216
+ export interface OpenAIClientLike<TParams = OpenAIChatCompletionCreateParamsLike, TOptions = Record<string, any>, TResponse = any, TChunk = any> {
217
+ chat: {
218
+ completions: OpenAIChatCompletionsLike<TParams, TOptions, TResponse, TChunk>;
219
+ [key: string]: any;
220
+ };
221
+ [key: string]: any;
222
+ }
205
223
  export type TraceServer = {
206
224
  broadcast(event: TraceEvent | UIReloadEvent): void;
207
225
  close(): void;