@botpress/evals 1.19.0-beta.9 → 2.0.0-alpha.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.
Files changed (85) hide show
  1. package/dist/client.d.ts +21 -9
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +103 -36
  4. package/dist/client.js.map +4 -4
  5. package/dist/definition.d.ts +9 -2
  6. package/dist/definition.d.ts.map +1 -1
  7. package/dist/definition.js.map +2 -2
  8. package/dist/graders/index.js +21 -74
  9. package/dist/graders/index.js.map +2 -2
  10. package/dist/graders/llm.d.ts +12 -12
  11. package/dist/graders/llm.d.ts.map +1 -1
  12. package/dist/graders/llm.js +30 -75
  13. package/dist/graders/llm.js.map +2 -2
  14. package/dist/graders/response.js +16 -65
  15. package/dist/graders/response.js.map +2 -2
  16. package/dist/index.d.ts +3 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +10 -0
  19. package/dist/index.js.map +3 -3
  20. package/dist/loader.js.map +2 -2
  21. package/dist/manifest.d.ts +13 -0
  22. package/dist/manifest.d.ts.map +1 -0
  23. package/dist/runner.d.ts +5 -1
  24. package/dist/runner.d.ts.map +1 -1
  25. package/dist/runner.js +316 -183
  26. package/dist/runner.js.map +4 -4
  27. package/dist/spans/index.d.ts +5 -0
  28. package/dist/spans/index.d.ts.map +1 -0
  29. package/dist/spans/index.js +725 -0
  30. package/dist/spans/index.js.map +7 -0
  31. package/dist/spans/local-span-source.d.ts +19 -0
  32. package/dist/spans/local-span-source.d.ts.map +1 -0
  33. package/dist/spans/local-span-source.js +543 -0
  34. package/dist/spans/local-span-source.js.map +7 -0
  35. package/dist/spans/span-source.d.ts +40 -0
  36. package/dist/spans/span-source.d.ts.map +1 -0
  37. package/dist/spans/span-source.js +1 -0
  38. package/dist/{sse-collector.d.ts → spans/sse-collector.d.ts} +44 -5
  39. package/dist/spans/sse-collector.d.ts.map +1 -0
  40. package/dist/{sse-collector.js → spans/sse-collector.js} +97 -20
  41. package/dist/spans/sse-collector.js.map +7 -0
  42. package/dist/spans/trace.d.ts.map +1 -0
  43. package/dist/spans/trace.js.map +7 -0
  44. package/dist/spans/vortex-span-source.d.ts +42 -0
  45. package/dist/spans/vortex-span-source.d.ts.map +1 -0
  46. package/dist/spans/vortex-span-source.js +184 -0
  47. package/dist/spans/vortex-span-source.js.map +7 -0
  48. package/dist/stores/eval-definition-loader.d.ts +4 -0
  49. package/dist/stores/eval-definition-loader.d.ts.map +1 -0
  50. package/dist/stores/eval-definition-loader.js +142 -0
  51. package/dist/stores/eval-definition-loader.js.map +7 -0
  52. package/dist/stores/eval-store.d.ts +61 -0
  53. package/dist/stores/eval-store.d.ts.map +1 -0
  54. package/dist/stores/eval-store.js +1 -0
  55. package/dist/stores/eval-store.js.map +7 -0
  56. package/dist/stores/index.d.ts +8 -0
  57. package/dist/stores/index.d.ts.map +1 -0
  58. package/dist/stores/index.js +1156 -0
  59. package/dist/stores/index.js.map +7 -0
  60. package/dist/stores/local-eval-store.d.ts +66 -0
  61. package/dist/stores/local-eval-store.d.ts.map +1 -0
  62. package/dist/stores/local-eval-store.js +595 -0
  63. package/dist/stores/local-eval-store.js.map +7 -0
  64. package/dist/stores/vortex-eval-store-entry.d.ts +6 -0
  65. package/dist/stores/vortex-eval-store-entry.d.ts.map +1 -0
  66. package/dist/stores/vortex-eval-store-entry.js +702 -0
  67. package/dist/stores/vortex-eval-store-entry.js.map +7 -0
  68. package/dist/stores/vortex-eval-store.d.ts +78 -0
  69. package/dist/stores/vortex-eval-store.d.ts.map +1 -0
  70. package/dist/stores/vortex-eval-store.js +562 -0
  71. package/dist/stores/vortex-eval-store.js.map +7 -0
  72. package/dist/transformer.d.ts +2 -2
  73. package/dist/transformer.d.ts.map +1 -1
  74. package/dist/transformer.js +1 -1
  75. package/dist/transformer.js.map +2 -2
  76. package/dist/types.d.ts +38 -33
  77. package/dist/types.d.ts.map +1 -1
  78. package/dist/types.js.map +2 -2
  79. package/package.json +66 -6
  80. package/dist/sse-collector.d.ts.map +0 -1
  81. package/dist/sse-collector.js.map +0 -7
  82. package/dist/trace.d.ts.map +0 -1
  83. /package/dist/{trace.js.map → spans/span-source.js.map} +0 -0
  84. /package/dist/{trace.d.ts → spans/trace.d.ts} +0 -0
  85. /package/dist/{trace.js → spans/trace.js} +0 -0
package/dist/runner.js CHANGED
@@ -1,6 +1,3 @@
1
- // src/runner.ts
2
- import { Client as BpClient2 } from "@botpress/client";
3
-
4
1
  // src/definition.ts
5
2
  var Eval = class {
6
3
  name;
@@ -26,10 +23,6 @@ var Eval = class {
26
23
  // src/types.ts
27
24
  var defaultLogger = console;
28
25
 
29
- // src/client.ts
30
- import { Client as BpClient } from "@botpress/client";
31
- import { createRequire } from "module";
32
-
33
26
  // ../analytics/src/errors.ts
34
27
  var AdkError = class extends Error {
35
28
  /**
@@ -79,30 +72,27 @@ var EvalRunnerError = class extends AdkError {
79
72
  };
80
73
 
81
74
  // src/client.ts
82
- var defaultChatClient = null;
83
- function getDefaultChatClient() {
84
- if (defaultChatClient) return defaultChatClient;
85
- const require2 = createRequire(import.meta.url);
86
- const mod = require2("@botpress/chat");
87
- const ChatClient = mod.default?.Client ?? mod.Client;
88
- if (!ChatClient) {
89
- throw new EvalRunnerError({
90
- code: "CHAT_CLIENT_MISSING",
91
- message: "@botpress/chat did not expose a Client export. Pass EvalRunnerConfig.chatClient explicitly."
92
- });
93
- }
94
- return ChatClient;
95
- }
75
+ var TRACE_STREAM_PREFLIGHT_TIMEOUT_MS = 1e4;
96
76
  var ChatSession = class {
97
- constructor(webhookId, chatClient) {
98
- this.webhookId = webhookId;
99
- this.chatClient = chatClient;
77
+ constructor(bpClient, botId, _chatWebhookId, _chatBaseUrl, _chatClient) {
78
+ this.bpClient = bpClient;
79
+ this.botId = botId;
80
+ this._chatWebhookId = _chatWebhookId;
81
+ this._chatBaseUrl = _chatBaseUrl;
82
+ this._chatClient = _chatClient;
100
83
  }
101
84
  client = null;
102
85
  conversationId = null;
103
86
  async connect() {
104
- const ChatClient = this.chatClient ?? getDefaultChatClient();
105
- this.client = await ChatClient.connect({ webhookId: this.webhookId });
87
+ const webhookId = this._chatWebhookId ?? await discoverWebhookId(this.bpClient, this.botId);
88
+ const ChatClientCtor = this._chatClient ?? (await import(
89
+ /* webpackIgnore: true */
90
+ "@botpress/chat"
91
+ )).Client;
92
+ this.client = await ChatClientCtor.connect({
93
+ webhookId,
94
+ ...this._chatBaseUrl ? { baseApiUrl: this._chatBaseUrl } : {}
95
+ });
106
96
  }
107
97
  /** Invariant guard — calling any session method before connect() is a runner bug. */
108
98
  assertConnected() {
@@ -126,6 +116,16 @@ var ChatSession = class {
126
116
  }
127
117
  return this.conversationId;
128
118
  }
119
+ /**
120
+ * Open a fresh conversation under the same user (the user is fixed at
121
+ * connect()) and make it active. Returns the new conversation id.
122
+ */
123
+ async newConversation() {
124
+ const client = this.assertConnected();
125
+ const conv = await client.createConversation({});
126
+ this.conversationId = conv.conversation.id;
127
+ return conv.conversation.id;
128
+ }
129
129
  /**
130
130
  * Send a user message. Fire-and-forget — observation is via SSECollector.
131
131
  */
@@ -134,17 +134,13 @@ var ChatSession = class {
134
134
  const conversationId = await this.ensureConversation();
135
135
  await client.createMessage({ conversationId, payload: { type: "text", text: message } });
136
136
  }
137
- /**
138
- * Send an event. Fire-and-forget — observation is via SSECollector.
139
- */
140
- async sendEvent(type, payload) {
137
+ async sendEvent(payload) {
141
138
  const client = this.assertConnected();
142
139
  const conversationId = await this.ensureConversation();
143
- await client.createEvent({ type, payload, conversationId });
140
+ await client.createEvent({ payload, conversationId });
144
141
  }
145
142
  };
146
- async function discoverWebhookId(botId, token, apiUrl) {
147
- const client = new BpClient({ token, botId, apiUrl });
143
+ async function discoverWebhookId(client, botId) {
148
144
  const { bot } = await client.getBot({ id: botId });
149
145
  const integrations = bot.integrations || {};
150
146
  const chat = Object.values(integrations).find((int) => int.name === "chat");
@@ -156,7 +152,7 @@ async function discoverWebhookId(botId, token, apiUrl) {
156
152
  "The `chat` integration is not installed on this bot \u2014 evals require it.",
157
153
  "",
158
154
  "To fix:",
159
- " 1. Install it: adk add chat",
155
+ " 1. Install it: adk integrations add chat",
160
156
  " 2. Redeploy: adk dev (or: adk deploy)"
161
157
  ].join("\n"),
162
158
  expected: true
@@ -164,6 +160,77 @@ async function discoverWebhookId(botId, token, apiUrl) {
164
160
  }
165
161
  return webhookId;
166
162
  }
163
+ async function assertTraceStreamReadable(devServerUrl, headers = {}, timeoutMs = TRACE_STREAM_PREFLIGHT_TIMEOUT_MS) {
164
+ const url = `${devServerUrl.replace(/\/$/, "")}/api/traces/stream?count=1`;
165
+ const abortController = new AbortController();
166
+ let timedOut = false;
167
+ let reader = null;
168
+ const timer = setTimeout(() => {
169
+ timedOut = true;
170
+ abortController.abort();
171
+ }, timeoutMs);
172
+ try {
173
+ const res = await fetch(url, {
174
+ headers: { ...headers, Accept: "text/event-stream" },
175
+ signal: abortController.signal
176
+ });
177
+ if (!res.ok) {
178
+ throw traceStreamPreflightError(`Trace stream pre-flight failed: ${res.status} ${res.statusText}`);
179
+ }
180
+ if (!res.body) {
181
+ throw traceStreamPreflightError("Trace stream pre-flight failed: response had no body.");
182
+ }
183
+ reader = res.body.getReader();
184
+ const decoder = new TextDecoder();
185
+ let buffer = "";
186
+ while (true) {
187
+ const { done, value } = await reader.read();
188
+ if (done) {
189
+ throw traceStreamPreflightError("Trace stream pre-flight failed: stream closed before snapshot.");
190
+ }
191
+ buffer += decoder.decode(value, { stream: true });
192
+ const parts = buffer.split("\n\n");
193
+ buffer = parts.pop() ?? "";
194
+ for (const part of parts) {
195
+ if (parseSSEEventName(part) === "snapshot") {
196
+ return;
197
+ }
198
+ }
199
+ }
200
+ } catch (err) {
201
+ if (err instanceof EvalRunnerError) {
202
+ throw err;
203
+ }
204
+ if (timedOut) {
205
+ throw traceStreamPreflightError(
206
+ `Trace stream pre-flight timed out after ${timeoutMs}ms before receiving a snapshot.`
207
+ );
208
+ }
209
+ throw traceStreamPreflightError(
210
+ `Trace stream pre-flight failed: ${err instanceof Error ? err.message : String(err)}`
211
+ );
212
+ } finally {
213
+ clearTimeout(timer);
214
+ reader?.cancel().catch(() => {
215
+ });
216
+ }
217
+ }
218
+ function parseSSEEventName(block) {
219
+ for (const line of block.split("\n")) {
220
+ if (line.startsWith("event:")) {
221
+ return line.slice("event:".length).trim();
222
+ }
223
+ }
224
+ return null;
225
+ }
226
+ function traceStreamPreflightError(message) {
227
+ return new EvalRunnerError({
228
+ code: "SSE_CONNECT_FAILED",
229
+ message,
230
+ expected: true,
231
+ suggestion: "Make sure the dev server is running (`adk dev`) and trace streaming is available."
232
+ });
233
+ }
167
234
  async function assertChatChannelBound(devServerUrl, headers = {}, logger = defaultLogger) {
168
235
  let conversations = [];
169
236
  try {
@@ -201,13 +268,17 @@ async function assertChatChannelBound(devServerUrl, headers = {}, logger = defau
201
268
  });
202
269
  }
203
270
 
204
- // src/sse-collector.ts
271
+ // src/spans/sse-collector.ts
205
272
  function spanKey(span) {
206
273
  return `${span.id.trace}:${span.id.span}`;
207
274
  }
208
275
  function spanData(span) {
209
276
  return span.data && typeof span.data === "object" ? span.data : {};
210
277
  }
278
+ var TURN_HANDLER_SPAN_NAMES = /* @__PURE__ */ new Set(["handler.conversation", "handler.event"]);
279
+ function isTurnHandlerSpan(span) {
280
+ return TURN_HANDLER_SPAN_NAMES.has(span.name);
281
+ }
211
282
  var SSECollector = class _SSECollector {
212
283
  devServerUrl;
213
284
  headers;
@@ -225,6 +296,10 @@ var SSECollector = class _SSECollector {
225
296
  connected = false;
226
297
  /** Error from the SSE stream if it died unexpectedly. */
227
298
  streamError = null;
299
+ /** Passive counters used only to explain timeout failures. */
300
+ updateEventCount = 0;
301
+ keepaliveEventCount = 0;
302
+ lastEvent = null;
228
303
  /** Set while a settle phase is in flight; called by disconnect() to short-circuit. */
229
304
  _cancelSettle = null;
230
305
  constructor(devServerUrl, headers = {}) {
@@ -240,9 +315,10 @@ var SSECollector = class _SSECollector {
240
315
  const attributeValue = "conversationId" in filter ? filter.conversationId : filter.workflowId;
241
316
  const url = `${this.devServerUrl}/api/traces/stream?attributeName=${encodeURIComponent(attributeName)}&attributeValue=${encodeURIComponent(attributeValue)}`;
242
317
  this.abortController = new AbortController();
318
+ const myController = this.abortController;
243
319
  const response = await fetch(url, {
244
320
  headers: { ...this.headers, Accept: "text/event-stream" },
245
- signal: this.abortController.signal
321
+ signal: myController.signal
246
322
  });
247
323
  if (!response.ok) {
248
324
  throw new EvalRunnerError({
@@ -257,28 +333,48 @@ var SSECollector = class _SSECollector {
257
333
  }
258
334
  this.connected = true;
259
335
  this._consumeStream(response.body).catch((err) => {
260
- if (this.connected) {
336
+ if (this.connected && this.abortController === myController) {
261
337
  this.streamError = err instanceof Error ? err : new Error(String(err));
262
338
  }
263
339
  });
264
340
  await this._waitForSnapshot();
265
341
  }
342
+ /**
343
+ * Re-point at a new conversation, keeping `spanMap` and `seenHandlerKeys` (so
344
+ * the earlier conversation stays visible and its completed handler can't
345
+ * satisfy the next turn). Resets the snapshot latch so connect() awaits the
346
+ * new conversation's snapshot.
347
+ */
348
+ async repoint(filter) {
349
+ this.connected = false;
350
+ this.abortController?.abort();
351
+ this.abortController = null;
352
+ this._cancelSettle?.();
353
+ this._cancelSettle = null;
354
+ this.waiters = [];
355
+ this.snapshotReceived = false;
356
+ this.snapshotResolvers = [];
357
+ this.streamError = null;
358
+ await this.connect(filter);
359
+ }
266
360
  /**
267
361
  * Mark the beginning of a new turn.
268
- * Records which handler.conversation span IDs have been seen so far,
269
- * so waitForTurnComplete() only resolves on NEW handler completions.
362
+ * Records which turn-handler span IDs (handler.conversation / handler.event)
363
+ * have been seen so far, so waitForTurnComplete() only resolves on NEW
364
+ * handler completions.
270
365
  */
271
366
  startTurn() {
272
367
  this.turnStartKeys = new Set(this.spanMap.keys());
273
368
  for (const [key, span] of this.spanMap) {
274
- if (span.name === "handler.conversation") {
369
+ if (isTurnHandlerSpan(span)) {
275
370
  this.seenHandlerKeys.add(key);
276
371
  }
277
372
  }
278
373
  }
279
374
  static SETTLE_MS = 100;
280
375
  /**
281
- * Wait for a handler.conversation span to reach terminal status (ok or error).
376
+ * Wait for a turn-handler span (handler.conversation or handler.event) to
377
+ * reach terminal status (ok or error).
282
378
  * Only considers spans NOT seen before the last startTurn() call.
283
379
  *
284
380
  * After the handler closes, waits for a quiet period (no new spans for SETTLE_MS)
@@ -287,8 +383,10 @@ var SSECollector = class _SSECollector {
287
383
  * spans may arrive slightly after the handler close event.
288
384
  */
289
385
  async waitForTurnComplete(opts) {
290
- if (this._hasNewCompletedHandler()) {
291
- return this._settleAfterHandler();
386
+ const accept = opts.acceptSpanNames;
387
+ const settle = { quietMs: opts.settleQuietMs, maxMs: opts.settleMaxMs };
388
+ if (this._hasNewCompletedHandler(accept)) {
389
+ return this._settleAfterHandler(settle);
292
390
  }
293
391
  if (opts.abortSignal?.aborted) {
294
392
  throw new Error("waitForTurnComplete aborted");
@@ -297,8 +395,13 @@ var SSECollector = class _SSECollector {
297
395
  const timer = setTimeout(() => {
298
396
  this._removeWaiter(waiter);
299
397
  opts.abortSignal?.removeEventListener("abort", onAbort);
300
- const reason = this.streamError ? `SSE stream died: ${this.streamError.message}` : "no handler.conversation span completed";
301
- reject(new Error(`waitForTurnComplete timed out after ${opts.timeout}ms \u2014 ${reason}`));
398
+ const expected = accept ? [...accept].join(" or ") : "handler.conversation or handler.event";
399
+ const reason = this.streamError ? `SSE stream died: ${this.streamError.message}` : `no ${expected} span completed`;
400
+ reject(
401
+ new Error(
402
+ `waitForTurnComplete timed out after ${opts.timeout}ms \u2014 ${reason}; diagnostics: ${this._formatTurnDiagnostics()}`
403
+ )
404
+ );
302
405
  }, opts.timeout);
303
406
  const onAbort = () => {
304
407
  clearTimeout(timer);
@@ -307,11 +410,11 @@ var SSECollector = class _SSECollector {
307
410
  };
308
411
  opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
309
412
  const waiter = (_span) => {
310
- if (this._hasNewCompletedHandler()) {
413
+ if (this._hasNewCompletedHandler(accept)) {
311
414
  clearTimeout(timer);
312
415
  this._removeWaiter(waiter);
313
416
  opts.abortSignal?.removeEventListener("abort", onAbort);
314
- this._settleAfterHandler().then(resolve2, reject);
417
+ this._settleAfterHandler(settle).then(resolve2, reject);
315
418
  }
316
419
  };
317
420
  this.waiters.push(waiter);
@@ -325,7 +428,9 @@ var SSECollector = class _SSECollector {
325
428
  * Exposes a cancellation hook via _cancelSettle so disconnect() can short-
326
429
  * circuit an in-flight settle phase and avoid leaking timers for up to 2s.
327
430
  */
328
- _settleAfterHandler() {
431
+ _settleAfterHandler(settle) {
432
+ const quietMs = settle?.quietMs ?? _SSECollector.SETTLE_MS;
433
+ const maxMs = settle?.maxMs ?? _SSECollector.MAX_SETTLE_MS;
329
434
  return new Promise((resolve2) => {
330
435
  let resolved = false;
331
436
  const done = () => {
@@ -337,12 +442,12 @@ var SSECollector = class _SSECollector {
337
442
  this._cancelSettle = null;
338
443
  resolve2();
339
444
  };
340
- const hardCap = setTimeout(done, _SSECollector.MAX_SETTLE_MS);
341
- let settleTimer = setTimeout(done, _SSECollector.SETTLE_MS);
445
+ const hardCap = setTimeout(done, maxMs);
446
+ let settleTimer = setTimeout(done, quietMs);
342
447
  const settleWaiter = () => {
343
448
  if (resolved) return;
344
449
  clearTimeout(settleTimer);
345
- settleTimer = setTimeout(done, _SSECollector.SETTLE_MS);
450
+ settleTimer = setTimeout(done, quietMs);
346
451
  };
347
452
  this.waiters.push(settleWaiter);
348
453
  this._cancelSettle = done;
@@ -507,6 +612,7 @@ ${value}` : value;
507
612
  * Dispatch a parsed SSE event to the appropriate handler.
508
613
  */
509
614
  _handleEvent(eventName, payload) {
615
+ this._recordEvent(eventName);
510
616
  if (eventName === "snapshot") {
511
617
  const data2 = payload;
512
618
  const spans = Array.isArray(data2.spans) ? data2.spans : [];
@@ -526,6 +632,44 @@ ${value}` : value;
526
632
  }
527
633
  }
528
634
  }
635
+ _recordEvent(eventName) {
636
+ this.lastEvent = { name: eventName, at: Date.now() };
637
+ if (eventName === "update") {
638
+ this.updateEventCount++;
639
+ } else if (eventName === "keepalive") {
640
+ this.keepaliveEventCount++;
641
+ }
642
+ }
643
+ _formatTurnDiagnostics() {
644
+ const turnSpans = this.getTurnSpans();
645
+ const spanNames = /* @__PURE__ */ new Set();
646
+ const handlerStatuses = /* @__PURE__ */ new Set();
647
+ for (const span of turnSpans) {
648
+ spanNames.add(span.name);
649
+ if (isTurnHandlerSpan(span)) {
650
+ handlerStatuses.add(span.status);
651
+ }
652
+ }
653
+ const lastEvent = this.lastEvent ? `${this.lastEvent.name} ${Date.now() - this.lastEvent.at}ms ago` : "none";
654
+ return [
655
+ `snapshotReceived=${this.snapshotReceived}`,
656
+ `totalUpdates=${this.updateEventCount}`,
657
+ `totalKeepalives=${this.keepaliveEventCount}`,
658
+ `lastEvent=${lastEvent}`,
659
+ `totalSpans=${this.spanMap.size}`,
660
+ `turnSpans=${turnSpans.length}`,
661
+ `turnSpanNames=${this._formatSet(spanNames)}`,
662
+ `turnHandlerStatuses=${this._formatSet(handlerStatuses)}`
663
+ ].join(", ");
664
+ }
665
+ _formatSet(values, limit = 12) {
666
+ if (values.size === 0) {
667
+ return "none";
668
+ }
669
+ const items = Array.from(values);
670
+ const suffix = items.length > limit ? `|+${items.length - limit} more` : "";
671
+ return `${items.slice(0, limit).join("|")}${suffix}`;
672
+ }
529
673
  /**
530
674
  * Insert or replace a span in the map (dedup by trace+span ID).
531
675
  */
@@ -551,7 +695,7 @@ ${value}` : value;
551
695
  }
552
696
  }
553
697
  /**
554
- * Check if there is a handler.conversation span that:
698
+ * Check if there is a turn-handler span (handler.conversation / handler.event) that:
555
699
  * 1. Was NOT seen before startTurn()
556
700
  * 2. Has terminal status (ok or error)
557
701
  *
@@ -560,9 +704,9 @@ ${value}` : value;
560
704
  * waitForTurnComplete check (which would otherwise resolve instantly
561
705
  * against stale data instead of waiting for the new turn's handler).
562
706
  */
563
- _hasNewCompletedHandler() {
707
+ _hasNewCompletedHandler(acceptSpanNames) {
564
708
  for (const [key, span] of this.spanMap) {
565
- if (span.name === "handler.conversation" && !this.seenHandlerKeys.has(key) && (span.status === "ok" || span.status === "error")) {
709
+ if (isTurnHandlerSpan(span) && (!acceptSpanNames || acceptSpanNames.has(span.name)) && !this.seenHandlerKeys.has(key) && (span.status === "ok" || span.status === "error")) {
566
710
  this.seenHandlerKeys.add(key);
567
711
  return true;
568
712
  }
@@ -589,6 +733,38 @@ ${value}` : value;
589
733
  }
590
734
  };
591
735
 
736
+ // src/spans/local-span-source.ts
737
+ var LocalSpanSource = class {
738
+ collector;
739
+ constructor(devServerUrl, headers = {}) {
740
+ this.collector = new SSECollector(devServerUrl, headers);
741
+ }
742
+ async connect(filter) {
743
+ await this.collector.connect(filter);
744
+ }
745
+ async repoint(filter) {
746
+ await this.collector.repoint(filter);
747
+ }
748
+ startTurn() {
749
+ this.collector.startTurn();
750
+ }
751
+ async waitForTurnComplete(opts) {
752
+ await this.collector.waitForTurnComplete(opts);
753
+ }
754
+ async waitForWorkflow(name, opts) {
755
+ await this.collector.waitForWorkflow(name, opts);
756
+ }
757
+ getTurnSpans() {
758
+ return this.collector.getTurnSpans();
759
+ }
760
+ getAllSpans() {
761
+ return this.collector.getAllSpans();
762
+ }
763
+ disconnect() {
764
+ this.collector.disconnect();
765
+ }
766
+ };
767
+
592
768
  // src/transformer.ts
593
769
  function data(span) {
594
770
  return span.data && typeof span.data === "object" ? span.data : {};
@@ -676,7 +852,7 @@ function extractWorkflowSpans(spans) {
676
852
  }
677
853
  function extractHandler(spans) {
678
854
  const handler = spans.find(
679
- (s) => (s.name === "handler.conversation" || s.name === "handler.workflow") && s.status !== "running"
855
+ (s) => (s.name === "handler.conversation" || s.name === "handler.event" || s.name === "handler.workflow") && s.status !== "running"
680
856
  );
681
857
  if (!handler) {
682
858
  return { handlerDuration: 0, handlerStatus: "ok" };
@@ -688,55 +864,26 @@ function extractHandler(spans) {
688
864
  }
689
865
 
690
866
  // src/graders/llm.ts
691
- import { Client } from "@botpress/client";
692
867
  import { Cognitive } from "@botpress/cognitive";
693
- var DEFAULT_PASS_THRESHOLD = 3;
694
- function buildJudgeSystemPrompt(passThreshold) {
695
- return `You are an evaluation judge for a chatbot. You will be given:
696
- - The user's message
697
- - The bot's response
698
- - Grading criteria
699
-
700
- Your job is to determine if the bot's response meets the criteria.
701
-
702
- Respond with a JSON object:
703
- {
704
- "pass": true/false,
705
- "score": 1-5,
706
- "reason": "brief explanation"
707
- }
708
-
709
- Scoring guide:
710
- - 5: Fully meets criteria, excellent response
711
- - 4: Mostly meets criteria, minor issues
712
- - 3: Partially meets criteria, notable gaps
713
- - 2: Barely meets criteria, significant issues
714
- - 1: Does not meet criteria
715
-
716
- A score of ${passThreshold} or above is a pass.`;
717
- }
868
+ import { Zai } from "@botpress/zai";
718
869
  var MODEL_ALIASES = ["fast", "best"];
719
- var _cognitive = null;
870
+ var _zai = null;
720
871
  var _availableModels = [];
721
872
  var _judgeModel = "fast";
873
+ var _failClosed = true;
722
874
  var _log = defaultLogger;
723
875
  function _isModelValid(model) {
724
876
  if (MODEL_ALIASES.includes(model)) return true;
725
877
  if (_availableModels.length === 0) return false;
726
878
  return _availableModels.includes(model);
727
879
  }
728
- async function initLLMJudge(credentials, options) {
880
+ async function initLLMJudge(client, options) {
729
881
  const log = options?.logger ?? defaultLogger;
730
882
  _log = log;
731
- const client = new Client({
732
- token: credentials.token,
733
- apiUrl: credentials.apiUrl,
734
- botId: credentials.botId,
735
- ...credentials.workspaceId !== void 0 ? { workspaceId: credentials.workspaceId } : {}
736
- });
737
- _cognitive = new Cognitive({ client, __experimental_beta: true });
883
+ _failClosed = options?.failClosed ?? true;
884
+ const cognitive = new Cognitive({ client, __experimental_beta: true });
738
885
  try {
739
- _availableModels = Array.from((await _cognitive.fetchRemoteModels()).keys());
886
+ _availableModels = Array.from((await cognitive.fetchRemoteModels()).keys());
740
887
  } catch (err) {
741
888
  log.warn(`Failed to fetch available models: ${err.message}. LLM judge will be unavailable.`);
742
889
  _availableModels = [];
@@ -747,66 +894,42 @@ async function initLLMJudge(credentials, options) {
747
894
  } else if (!_isModelValid(_judgeModel)) {
748
895
  log.warn(`Configured LLM judge model "${_judgeModel}" is invalid. Run "adk models" to list available models.`);
749
896
  }
897
+ _zai = new Zai({ client: cognitive, modelId: _judgeModel });
750
898
  }
751
899
  async function gradeLLMJudge(botResponse, criteria, context) {
752
- const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD));
900
+ const assertion = `llm_judge: "${criteria}"`;
753
901
  try {
754
- if (!_cognitive) {
902
+ if (!_zai) {
755
903
  _log.warn(`LLM judge unavailable for "${criteria}": no credentials configured (initLLMJudge not called)`);
756
904
  return {
757
- assertion: `llm_judge: "${criteria}"`,
758
- pass: false,
905
+ assertion,
906
+ pass: !_failClosed,
907
+ skipped: true,
759
908
  expected: criteria,
760
909
  actual: "FAILED \u2014 LLM judge unavailable: no credentials configured"
761
910
  };
762
911
  }
763
912
  if (!_isModelValid(_judgeModel)) {
764
913
  return {
765
- assertion: `llm_judge: "${criteria}"`,
914
+ assertion,
766
915
  pass: false,
767
916
  expected: criteria,
768
917
  actual: "FAILED \u2014 invalid LLM judge model"
769
918
  };
770
919
  }
771
- const { output } = await _cognitive.generateContent({
772
- model: _judgeModel,
773
- temperature: 0,
774
- responseFormat: "json_object",
775
- systemPrompt: buildJudgeSystemPrompt(threshold),
776
- messages: [
777
- {
778
- role: "user",
779
- content: `User message: ${context.userMessage}
780
-
781
- Bot response: ${botResponse}
782
-
783
- Criteria: ${criteria}`
784
- }
785
- ]
786
- });
787
- const rawContent = output?.choices?.[0]?.content;
788
- const content = typeof rawContent === "string" ? rawContent : null;
789
- if (!content) {
790
- _log.warn(`LLM judge returned an empty response for "${criteria}"`);
791
- return {
792
- assertion: `llm_judge: "${criteria}"`,
793
- pass: false,
794
- expected: criteria,
795
- actual: "FAILED \u2014 LLM judge returned empty response"
796
- };
797
- }
798
- const verdict = JSON.parse(content);
920
+ const { output } = await _zai.check({ userMessage: context.userMessage, botResponse }, criteria).result();
799
921
  return {
800
- assertion: `llm_judge: "${criteria}"`,
801
- pass: verdict.score >= threshold,
922
+ assertion,
923
+ pass: output.value,
802
924
  expected: criteria,
803
- actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`
925
+ actual: `${output.value ? "Pass" : "Fail"} \u2014 ${output.explanation}`
804
926
  };
805
927
  } catch (err) {
806
928
  _log.warn(`LLM judge failed for "${criteria}": ${err.message}`);
807
929
  return {
808
- assertion: `llm_judge: "${criteria}"`,
809
- pass: false,
930
+ assertion,
931
+ pass: !_failClosed,
932
+ skipped: true,
810
933
  expected: criteria,
811
934
  actual: `FAILED \u2014 LLM judge unavailable: ${err.message}`
812
935
  };
@@ -1315,27 +1438,22 @@ async function runEval(evalDef, connection, options = {}) {
1315
1438
  const start = Date.now();
1316
1439
  const turns = [];
1317
1440
  let outcomeAssertions = [];
1318
- const collector = new SSECollector(devServerUrl, devServerHeaders);
1441
+ const collector = options.spanSource ?? new LocalSpanSource(devServerUrl, devServerHeaders);
1319
1442
  try {
1320
- const session = new ChatSession(connection.webhookId, options.chatClient);
1443
+ const session = new ChatSession(
1444
+ connection.client,
1445
+ connection.botId,
1446
+ options.chatWebhookId,
1447
+ options.chatBaseUrl,
1448
+ options.chatClient
1449
+ );
1321
1450
  await session.connect();
1322
- let bpClient = null;
1323
- const getBpClient = () => {
1324
- if (!bpClient) {
1325
- bpClient = new BpClient2({
1326
- token: connection.token,
1327
- botId: connection.botId,
1328
- apiUrl: connection.apiUrl
1329
- });
1330
- }
1331
- return bpClient;
1332
- };
1333
1451
  let conversationId = "";
1334
1452
  if (evalDef.setup?.state?.conversation || evalDef.setup?.workflow) {
1335
1453
  conversationId = await session.ensureConversation();
1336
1454
  }
1337
1455
  await seedEvalState(
1338
- getBpClient(),
1456
+ connection.client,
1339
1457
  {
1340
1458
  botId: connection.botId,
1341
1459
  userId: session.userId,
@@ -1344,7 +1462,7 @@ async function runEval(evalDef, connection, options = {}) {
1344
1462
  evalDef.setup
1345
1463
  );
1346
1464
  await triggerEvalWorkflow(
1347
- getBpClient(),
1465
+ connection.client,
1348
1466
  {
1349
1467
  userId: session.userId,
1350
1468
  ...conversationId ? { conversationId } : {}
@@ -1380,24 +1498,34 @@ async function runEval(evalDef, connection, options = {}) {
1380
1498
  if (!turn.user && !turn.event) {
1381
1499
  throw turnConfigError(`must have either 'user' or 'event'.`);
1382
1500
  }
1383
- const turnLabelForEvent = turn.event ? `[event: ${turn.event.type}]` : turn.user;
1384
- options.onProgress?.({
1501
+ const turnLabel = turn.event ? "[event]" : turn.user;
1502
+ await options.onProgress?.({
1385
1503
  type: "turn_start",
1386
1504
  evalName: evalDef.name,
1387
1505
  evalIndex: options.evalIndex ?? 0,
1388
- turnNumber: i + 1,
1506
+ turnIndex: i,
1389
1507
  totalTurns: evalDef.conversation.length,
1390
- userMessage: turnLabelForEvent
1508
+ userMessage: turnLabel
1391
1509
  });
1510
+ if (turn.newConversation && i > 0) {
1511
+ const newConversationId = await session.newConversation();
1512
+ await collector.repoint({ conversationId: newConversationId });
1513
+ }
1392
1514
  collector.startTurn();
1393
1515
  if (turn.event) {
1394
- await session.sendEvent(turn.event.type, turn.event.payload ?? {});
1516
+ await session.sendEvent(turn.event.payload);
1395
1517
  } else {
1396
1518
  await session.sendMessage(turn.user);
1397
1519
  }
1398
1520
  try {
1399
1521
  await collector.waitForTurnComplete({
1400
1522
  timeout: idleTimeout,
1523
+ acceptSpanNames: turn.event ? /* @__PURE__ */ new Set(["handler.event"]) : /* @__PURE__ */ new Set(["handler.conversation"]),
1524
+ // Long quiet window: a tool-started workflow and its callback only run
1525
+ // AFTER the handler closes; sending the next message into that window
1526
+ // can get it absorbed by their transcript saves and silently skipped.
1527
+ settleQuietMs: 1500,
1528
+ settleMaxMs: 15e3,
1401
1529
  ...options.signal !== void 0 ? { abortSignal: options.signal } : {}
1402
1530
  });
1403
1531
  } catch (err) {
@@ -1421,7 +1549,6 @@ async function runEval(evalDef, connection, options = {}) {
1421
1549
  const turnData = transformSpans(turnSpans);
1422
1550
  const botResponse = turnData.messages.join("\n");
1423
1551
  const botDuration = turnData.handlerDuration;
1424
- const turnLabel = turn.event ? `[event: ${turn.event.type}]` : turn.user;
1425
1552
  const evalStart = Date.now();
1426
1553
  let assertions = [];
1427
1554
  const noResponse = !turn.expectSilence && turnData.messages.length === 0;
@@ -1476,7 +1603,7 @@ async function runEval(evalDef, connection, options = {}) {
1476
1603
  const turnPass = assertions.every((a) => a.pass);
1477
1604
  const evalDuration = Date.now() - evalStart;
1478
1605
  const turnReport = {
1479
- turnNumber: i + 1,
1606
+ turnIndex: i,
1480
1607
  userMessage: turnLabel,
1481
1608
  botResponse,
1482
1609
  assertions,
@@ -1485,11 +1612,11 @@ async function runEval(evalDef, connection, options = {}) {
1485
1612
  evalDuration
1486
1613
  };
1487
1614
  turns.push(turnReport);
1488
- options.onProgress?.({
1615
+ await options.onProgress?.({
1489
1616
  type: "turn_complete",
1490
1617
  evalName: evalDef.name,
1491
1618
  evalIndex: options.evalIndex ?? 0,
1492
- turnNumber: i + 1,
1619
+ turnIndex: i,
1493
1620
  totalTurns: evalDef.conversation.length,
1494
1621
  turnReport
1495
1622
  });
@@ -1536,7 +1663,15 @@ async function runEval(evalDef, connection, options = {}) {
1536
1663
  } catch (err) {
1537
1664
  const adkErr = isAdkError(err) ? err : void 0;
1538
1665
  if (!adkErr || !adkErr.expected) {
1539
- logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${err.message}`);
1666
+ const errMsg = err.message ?? String(err);
1667
+ const errStack = err.stack ?? "";
1668
+ logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${errMsg}`);
1669
+ logger.error(` stack: ${errStack}`);
1670
+ logger.error(` eval: ${evalDef.name}, turn: ${turns.length}/${evalDef.conversation.length}`);
1671
+ logger.error(` connection: botId=${connection.botId}`);
1672
+ logger.error(
1673
+ ` spanSource: ${options.spanSource?.constructor?.name ?? "default"}, devServerUrl: ${options.devServerUrl ?? "n/a"}`
1674
+ );
1540
1675
  options.onException?.(err, { source: "evals", eval_name: evalDef.name });
1541
1676
  }
1542
1677
  return {
@@ -1554,9 +1689,15 @@ async function runEval(evalDef, connection, options = {}) {
1554
1689
  }
1555
1690
  async function runEvalSuite(config, filter) {
1556
1691
  const start = Date.now();
1557
- const runId = randomUUID().replace(/-/g, "").slice(0, 26);
1558
- const evalsDir = `${config.agentPath}/evals`;
1559
- const allEvals = await loadEvalsFromDir(evalsDir);
1692
+ const runId = config.runId ?? randomUUID().replace(/-/g, "").slice(0, 26);
1693
+ let allEvals;
1694
+ if (config.definitions) {
1695
+ allEvals = config.definitions;
1696
+ } else if (config.agentPath) {
1697
+ allEvals = await loadEvalsFromDir(`${config.agentPath}/evals`);
1698
+ } else {
1699
+ throw new Error("Either `definitions` or `agentPath` must be provided in EvalRunnerConfig");
1700
+ }
1560
1701
  const evals = filterEvals(allEvals, filter);
1561
1702
  if (evals.length === 0) {
1562
1703
  return {
@@ -1570,37 +1711,26 @@ async function runEvalSuite(config, filter) {
1570
1711
  ...filter !== void 0 ? { filter } : {}
1571
1712
  };
1572
1713
  }
1573
- await initLLMJudge(
1574
- {
1575
- token: config.credentials.token,
1576
- apiUrl: config.credentials.apiUrl,
1577
- botId: config.credentials.botId,
1578
- ...config.credentials.workspaceId !== void 0 ? { workspaceId: config.credentials.workspaceId } : {}
1579
- },
1580
- {
1581
- ...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
1582
- ...config.logger !== void 0 ? { logger: config.logger } : {}
1583
- }
1584
- );
1585
- let webhookId = config.credentials.webhookId;
1586
- if (!webhookId) {
1587
- webhookId = await discoverWebhookId(config.credentials.botId, config.credentials.token, config.credentials.apiUrl);
1588
- }
1714
+ await initLLMJudge(config.client, {
1715
+ ...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
1716
+ ...config.logger !== void 0 ? { logger: config.logger } : {}
1717
+ });
1589
1718
  const connection = {
1590
- webhookId,
1591
- botId: config.credentials.botId,
1592
- token: config.credentials.token,
1593
- apiUrl: config.credentials.apiUrl
1719
+ client: config.client,
1720
+ botId: config.botId
1594
1721
  };
1595
1722
  const devServerUrl = config.devServerUrl || DEFAULT_DEV_SERVER_URL;
1596
1723
  const devServerHeaders = config.devServerHeaders ?? {};
1597
- await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
1724
+ if (!config.createSpanSource) {
1725
+ await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
1726
+ await assertTraceStreamReadable(devServerUrl, devServerHeaders);
1727
+ }
1598
1728
  const reports = [];
1599
- config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
1729
+ await config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
1600
1730
  for (let i = 0; i < evals.length; i++) {
1601
1731
  if (config.signal?.aborted) break;
1602
1732
  const evalDef = evals[i];
1603
- config.onProgress?.({
1733
+ await config.onProgress?.({
1604
1734
  type: "eval_start",
1605
1735
  evalName: evalDef.name,
1606
1736
  index: i,
@@ -1616,10 +1746,13 @@ async function runEvalSuite(config, filter) {
1616
1746
  ...config.signal !== void 0 ? { signal: config.signal } : {},
1617
1747
  ...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {},
1618
1748
  ...config.logger !== void 0 ? { logger: config.logger } : {},
1619
- ...config.onException !== void 0 ? { onException: config.onException } : {}
1749
+ ...config.onException !== void 0 ? { onException: config.onException } : {},
1750
+ ...config.createSpanSource !== void 0 ? { spanSource: config.createSpanSource() } : {},
1751
+ ...config.chatWebhookId !== void 0 ? { chatWebhookId: config.chatWebhookId } : {},
1752
+ ...config.chatBaseUrl !== void 0 ? { chatBaseUrl: config.chatBaseUrl } : {}
1620
1753
  });
1621
1754
  reports.push(report);
1622
- config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
1755
+ await config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
1623
1756
  }
1624
1757
  const runReport = {
1625
1758
  id: runId,
@@ -1634,7 +1767,7 @@ async function runEvalSuite(config, filter) {
1634
1767
  if (config.signal?.aborted) {
1635
1768
  runReport.aborted = true;
1636
1769
  }
1637
- config.onProgress?.({ type: "suite_complete", report: runReport });
1770
+ await config.onProgress?.({ type: "suite_complete", report: runReport });
1638
1771
  return runReport;
1639
1772
  }
1640
1773
  export {