@botpress/evals 1.19.0-beta.9 → 2.0.0-alpha.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.
Files changed (86) hide show
  1. package/dist/client.d.ts +21 -9
  2. package/dist/client.d.ts.map +1 -1
  3. package/dist/client.js +104 -75
  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 +1 -39
  21. package/dist/loader.js.map +3 -3
  22. package/dist/manifest.d.ts +13 -0
  23. package/dist/manifest.d.ts.map +1 -0
  24. package/dist/runner.d.ts +5 -1
  25. package/dist/runner.d.ts.map +1 -1
  26. package/dist/runner.js +318 -227
  27. package/dist/runner.js.map +4 -4
  28. package/dist/spans/index.d.ts +5 -0
  29. package/dist/spans/index.d.ts.map +1 -0
  30. package/dist/spans/index.js +687 -0
  31. package/dist/spans/index.js.map +7 -0
  32. package/dist/spans/local-span-source.d.ts +19 -0
  33. package/dist/spans/local-span-source.d.ts.map +1 -0
  34. package/dist/spans/local-span-source.js +505 -0
  35. package/dist/spans/local-span-source.js.map +7 -0
  36. package/dist/spans/span-source.d.ts +40 -0
  37. package/dist/spans/span-source.d.ts.map +1 -0
  38. package/dist/spans/span-source.js +1 -0
  39. package/dist/{sse-collector.d.ts → spans/sse-collector.d.ts} +44 -5
  40. package/dist/spans/sse-collector.d.ts.map +1 -0
  41. package/dist/{sse-collector.js → spans/sse-collector.js} +98 -59
  42. package/dist/spans/sse-collector.js.map +7 -0
  43. package/dist/spans/trace.d.ts.map +1 -0
  44. package/dist/spans/trace.js.map +7 -0
  45. package/dist/spans/vortex-span-source.d.ts +42 -0
  46. package/dist/spans/vortex-span-source.d.ts.map +1 -0
  47. package/dist/spans/vortex-span-source.js +184 -0
  48. package/dist/spans/vortex-span-source.js.map +7 -0
  49. package/dist/stores/eval-definition-loader.d.ts +4 -0
  50. package/dist/stores/eval-definition-loader.d.ts.map +1 -0
  51. package/dist/stores/eval-definition-loader.js +104 -0
  52. package/dist/stores/eval-definition-loader.js.map +7 -0
  53. package/dist/stores/eval-store.d.ts +61 -0
  54. package/dist/stores/eval-store.d.ts.map +1 -0
  55. package/dist/stores/eval-store.js +1 -0
  56. package/dist/stores/eval-store.js.map +7 -0
  57. package/dist/stores/index.d.ts +8 -0
  58. package/dist/stores/index.d.ts.map +1 -0
  59. package/dist/stores/index.js +1118 -0
  60. package/dist/stores/index.js.map +7 -0
  61. package/dist/stores/local-eval-store.d.ts +66 -0
  62. package/dist/stores/local-eval-store.d.ts.map +1 -0
  63. package/dist/stores/local-eval-store.js +557 -0
  64. package/dist/stores/local-eval-store.js.map +7 -0
  65. package/dist/stores/vortex-eval-store-entry.d.ts +6 -0
  66. package/dist/stores/vortex-eval-store-entry.d.ts.map +1 -0
  67. package/dist/stores/vortex-eval-store-entry.js +664 -0
  68. package/dist/stores/vortex-eval-store-entry.js.map +7 -0
  69. package/dist/stores/vortex-eval-store.d.ts +78 -0
  70. package/dist/stores/vortex-eval-store.d.ts.map +1 -0
  71. package/dist/stores/vortex-eval-store.js +562 -0
  72. package/dist/stores/vortex-eval-store.js.map +7 -0
  73. package/dist/transformer.d.ts +2 -2
  74. package/dist/transformer.d.ts.map +1 -1
  75. package/dist/transformer.js +1 -1
  76. package/dist/transformer.js.map +2 -2
  77. package/dist/types.d.ts +38 -33
  78. package/dist/types.d.ts.map +1 -1
  79. package/dist/types.js.map +2 -2
  80. package/package.json +66 -6
  81. package/dist/sse-collector.d.ts.map +0 -1
  82. package/dist/sse-collector.js.map +0 -7
  83. package/dist/trace.d.ts.map +0 -1
  84. /package/dist/{trace.js.map → spans/span-source.js.map} +0 -0
  85. /package/dist/{trace.d.ts → spans/trace.d.ts} +0 -0
  86. /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,83 +23,33 @@ 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
- // ../analytics/src/errors.ts
34
- var AdkError = class extends Error {
35
- /**
36
- * Marker for cross-module-instance checks — `instanceof` breaks when a
37
- * package is duplicated in the dependency graph.
38
- * @internal
39
- */
40
- static __IS_ADK_BASE_ERROR = true;
41
- /**
42
- * Stable SCREAMING_SNAKE_CASE identifier, e.g. 'BOT_NOT_FOUND'. Subclasses
43
- * pin `Code` to a literal union (`class EvalRunnerError extends
44
- * AdkError<EvalErrorCode>`) so typos are compile errors and `code` narrows
45
- * in switches.
46
- */
47
- code;
48
- /**
49
- * True for user/environment conditions the developer can fix themselves
50
- * (validation, auth, connectivity). False for internal bugs. Defaults to
51
- * false so unclassified errors surface as bugs until someone triages them.
52
- */
53
- expected;
54
- /** Structured context. Sanitized before any telemetry leaves the machine. */
55
- details;
56
- /** Actionable next step shown to the user, e.g. "Run 'adk login' first." */
57
- suggestion;
58
- constructor(opts) {
59
- super(opts.message, opts.cause !== void 0 ? { cause: opts.cause } : void 0);
60
- this.name = this.constructor.name;
61
- this.code = opts.code;
62
- this.expected = opts.expected ?? false;
63
- if (opts.details !== void 0) {
64
- this.details = opts.details;
65
- }
66
- if (opts.suggestion !== void 0) {
67
- this.suggestion = opts.suggestion;
68
- }
69
- }
70
- };
71
- function isAdkError(error) {
72
- if (error instanceof AdkError) return true;
73
- if (!(error instanceof Error)) return false;
74
- return error.constructor?.__IS_ADK_BASE_ERROR === true;
75
- }
76
-
77
26
  // src/errors.ts
27
+ import { AdkError } from "@botpress/analytics";
78
28
  var EvalRunnerError = class extends AdkError {
79
29
  };
80
30
 
81
31
  // 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
- }
32
+ var TRACE_STREAM_PREFLIGHT_TIMEOUT_MS = 1e4;
96
33
  var ChatSession = class {
97
- constructor(webhookId, chatClient) {
98
- this.webhookId = webhookId;
99
- this.chatClient = chatClient;
34
+ constructor(bpClient, botId, _chatWebhookId, _chatBaseUrl, _chatClient) {
35
+ this.bpClient = bpClient;
36
+ this.botId = botId;
37
+ this._chatWebhookId = _chatWebhookId;
38
+ this._chatBaseUrl = _chatBaseUrl;
39
+ this._chatClient = _chatClient;
100
40
  }
101
41
  client = null;
102
42
  conversationId = null;
103
43
  async connect() {
104
- const ChatClient = this.chatClient ?? getDefaultChatClient();
105
- this.client = await ChatClient.connect({ webhookId: this.webhookId });
44
+ const webhookId = this._chatWebhookId ?? await discoverWebhookId(this.bpClient, this.botId);
45
+ const ChatClientCtor = this._chatClient ?? (await import(
46
+ /* webpackIgnore: true */
47
+ "@botpress/chat"
48
+ )).Client;
49
+ this.client = await ChatClientCtor.connect({
50
+ webhookId,
51
+ ...this._chatBaseUrl ? { baseApiUrl: this._chatBaseUrl } : {}
52
+ });
106
53
  }
107
54
  /** Invariant guard — calling any session method before connect() is a runner bug. */
108
55
  assertConnected() {
@@ -126,6 +73,16 @@ var ChatSession = class {
126
73
  }
127
74
  return this.conversationId;
128
75
  }
76
+ /**
77
+ * Open a fresh conversation under the same user (the user is fixed at
78
+ * connect()) and make it active. Returns the new conversation id.
79
+ */
80
+ async newConversation() {
81
+ const client = this.assertConnected();
82
+ const conv = await client.createConversation({});
83
+ this.conversationId = conv.conversation.id;
84
+ return conv.conversation.id;
85
+ }
129
86
  /**
130
87
  * Send a user message. Fire-and-forget — observation is via SSECollector.
131
88
  */
@@ -134,17 +91,13 @@ var ChatSession = class {
134
91
  const conversationId = await this.ensureConversation();
135
92
  await client.createMessage({ conversationId, payload: { type: "text", text: message } });
136
93
  }
137
- /**
138
- * Send an event. Fire-and-forget — observation is via SSECollector.
139
- */
140
- async sendEvent(type, payload) {
94
+ async sendEvent(payload) {
141
95
  const client = this.assertConnected();
142
96
  const conversationId = await this.ensureConversation();
143
- await client.createEvent({ type, payload, conversationId });
97
+ await client.createEvent({ payload, conversationId });
144
98
  }
145
99
  };
146
- async function discoverWebhookId(botId, token, apiUrl) {
147
- const client = new BpClient({ token, botId, apiUrl });
100
+ async function discoverWebhookId(client, botId) {
148
101
  const { bot } = await client.getBot({ id: botId });
149
102
  const integrations = bot.integrations || {};
150
103
  const chat = Object.values(integrations).find((int) => int.name === "chat");
@@ -156,7 +109,7 @@ async function discoverWebhookId(botId, token, apiUrl) {
156
109
  "The `chat` integration is not installed on this bot \u2014 evals require it.",
157
110
  "",
158
111
  "To fix:",
159
- " 1. Install it: adk add chat",
112
+ " 1. Install it: adk integrations add chat",
160
113
  " 2. Redeploy: adk dev (or: adk deploy)"
161
114
  ].join("\n"),
162
115
  expected: true
@@ -164,6 +117,77 @@ async function discoverWebhookId(botId, token, apiUrl) {
164
117
  }
165
118
  return webhookId;
166
119
  }
120
+ async function assertTraceStreamReadable(devServerUrl, headers = {}, timeoutMs = TRACE_STREAM_PREFLIGHT_TIMEOUT_MS) {
121
+ const url = `${devServerUrl.replace(/\/$/, "")}/api/traces/stream?count=1`;
122
+ const abortController = new AbortController();
123
+ let timedOut = false;
124
+ let reader = null;
125
+ const timer = setTimeout(() => {
126
+ timedOut = true;
127
+ abortController.abort();
128
+ }, timeoutMs);
129
+ try {
130
+ const res = await fetch(url, {
131
+ headers: { ...headers, Accept: "text/event-stream" },
132
+ signal: abortController.signal
133
+ });
134
+ if (!res.ok) {
135
+ throw traceStreamPreflightError(`Trace stream pre-flight failed: ${res.status} ${res.statusText}`);
136
+ }
137
+ if (!res.body) {
138
+ throw traceStreamPreflightError("Trace stream pre-flight failed: response had no body.");
139
+ }
140
+ reader = res.body.getReader();
141
+ const decoder = new TextDecoder();
142
+ let buffer = "";
143
+ while (true) {
144
+ const { done, value } = await reader.read();
145
+ if (done) {
146
+ throw traceStreamPreflightError("Trace stream pre-flight failed: stream closed before snapshot.");
147
+ }
148
+ buffer += decoder.decode(value, { stream: true });
149
+ const parts = buffer.split("\n\n");
150
+ buffer = parts.pop() ?? "";
151
+ for (const part of parts) {
152
+ if (parseSSEEventName(part) === "snapshot") {
153
+ return;
154
+ }
155
+ }
156
+ }
157
+ } catch (err) {
158
+ if (err instanceof EvalRunnerError) {
159
+ throw err;
160
+ }
161
+ if (timedOut) {
162
+ throw traceStreamPreflightError(
163
+ `Trace stream pre-flight timed out after ${timeoutMs}ms before receiving a snapshot.`
164
+ );
165
+ }
166
+ throw traceStreamPreflightError(
167
+ `Trace stream pre-flight failed: ${err instanceof Error ? err.message : String(err)}`
168
+ );
169
+ } finally {
170
+ clearTimeout(timer);
171
+ reader?.cancel().catch(() => {
172
+ });
173
+ }
174
+ }
175
+ function parseSSEEventName(block) {
176
+ for (const line of block.split("\n")) {
177
+ if (line.startsWith("event:")) {
178
+ return line.slice("event:".length).trim();
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+ function traceStreamPreflightError(message) {
184
+ return new EvalRunnerError({
185
+ code: "SSE_CONNECT_FAILED",
186
+ message,
187
+ expected: true,
188
+ suggestion: "Make sure the dev server is running (`adk dev`) and trace streaming is available."
189
+ });
190
+ }
167
191
  async function assertChatChannelBound(devServerUrl, headers = {}, logger = defaultLogger) {
168
192
  let conversations = [];
169
193
  try {
@@ -201,13 +225,17 @@ async function assertChatChannelBound(devServerUrl, headers = {}, logger = defau
201
225
  });
202
226
  }
203
227
 
204
- // src/sse-collector.ts
228
+ // src/spans/sse-collector.ts
205
229
  function spanKey(span) {
206
230
  return `${span.id.trace}:${span.id.span}`;
207
231
  }
208
232
  function spanData(span) {
209
233
  return span.data && typeof span.data === "object" ? span.data : {};
210
234
  }
235
+ var TURN_HANDLER_SPAN_NAMES = /* @__PURE__ */ new Set(["handler.conversation", "handler.event"]);
236
+ function isTurnHandlerSpan(span) {
237
+ return TURN_HANDLER_SPAN_NAMES.has(span.name);
238
+ }
211
239
  var SSECollector = class _SSECollector {
212
240
  devServerUrl;
213
241
  headers;
@@ -225,6 +253,10 @@ var SSECollector = class _SSECollector {
225
253
  connected = false;
226
254
  /** Error from the SSE stream if it died unexpectedly. */
227
255
  streamError = null;
256
+ /** Passive counters used only to explain timeout failures. */
257
+ updateEventCount = 0;
258
+ keepaliveEventCount = 0;
259
+ lastEvent = null;
228
260
  /** Set while a settle phase is in flight; called by disconnect() to short-circuit. */
229
261
  _cancelSettle = null;
230
262
  constructor(devServerUrl, headers = {}) {
@@ -240,9 +272,10 @@ var SSECollector = class _SSECollector {
240
272
  const attributeValue = "conversationId" in filter ? filter.conversationId : filter.workflowId;
241
273
  const url = `${this.devServerUrl}/api/traces/stream?attributeName=${encodeURIComponent(attributeName)}&attributeValue=${encodeURIComponent(attributeValue)}`;
242
274
  this.abortController = new AbortController();
275
+ const myController = this.abortController;
243
276
  const response = await fetch(url, {
244
277
  headers: { ...this.headers, Accept: "text/event-stream" },
245
- signal: this.abortController.signal
278
+ signal: myController.signal
246
279
  });
247
280
  if (!response.ok) {
248
281
  throw new EvalRunnerError({
@@ -257,28 +290,48 @@ var SSECollector = class _SSECollector {
257
290
  }
258
291
  this.connected = true;
259
292
  this._consumeStream(response.body).catch((err) => {
260
- if (this.connected) {
293
+ if (this.connected && this.abortController === myController) {
261
294
  this.streamError = err instanceof Error ? err : new Error(String(err));
262
295
  }
263
296
  });
264
297
  await this._waitForSnapshot();
265
298
  }
299
+ /**
300
+ * Re-point at a new conversation, keeping `spanMap` and `seenHandlerKeys` (so
301
+ * the earlier conversation stays visible and its completed handler can't
302
+ * satisfy the next turn). Resets the snapshot latch so connect() awaits the
303
+ * new conversation's snapshot.
304
+ */
305
+ async repoint(filter) {
306
+ this.connected = false;
307
+ this.abortController?.abort();
308
+ this.abortController = null;
309
+ this._cancelSettle?.();
310
+ this._cancelSettle = null;
311
+ this.waiters = [];
312
+ this.snapshotReceived = false;
313
+ this.snapshotResolvers = [];
314
+ this.streamError = null;
315
+ await this.connect(filter);
316
+ }
266
317
  /**
267
318
  * 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.
319
+ * Records which turn-handler span IDs (handler.conversation / handler.event)
320
+ * have been seen so far, so waitForTurnComplete() only resolves on NEW
321
+ * handler completions.
270
322
  */
271
323
  startTurn() {
272
324
  this.turnStartKeys = new Set(this.spanMap.keys());
273
325
  for (const [key, span] of this.spanMap) {
274
- if (span.name === "handler.conversation") {
326
+ if (isTurnHandlerSpan(span)) {
275
327
  this.seenHandlerKeys.add(key);
276
328
  }
277
329
  }
278
330
  }
279
331
  static SETTLE_MS = 100;
280
332
  /**
281
- * Wait for a handler.conversation span to reach terminal status (ok or error).
333
+ * Wait for a turn-handler span (handler.conversation or handler.event) to
334
+ * reach terminal status (ok or error).
282
335
  * Only considers spans NOT seen before the last startTurn() call.
283
336
  *
284
337
  * After the handler closes, waits for a quiet period (no new spans for SETTLE_MS)
@@ -287,8 +340,10 @@ var SSECollector = class _SSECollector {
287
340
  * spans may arrive slightly after the handler close event.
288
341
  */
289
342
  async waitForTurnComplete(opts) {
290
- if (this._hasNewCompletedHandler()) {
291
- return this._settleAfterHandler();
343
+ const accept = opts.acceptSpanNames;
344
+ const settle = { quietMs: opts.settleQuietMs, maxMs: opts.settleMaxMs };
345
+ if (this._hasNewCompletedHandler(accept)) {
346
+ return this._settleAfterHandler(settle);
292
347
  }
293
348
  if (opts.abortSignal?.aborted) {
294
349
  throw new Error("waitForTurnComplete aborted");
@@ -297,8 +352,13 @@ var SSECollector = class _SSECollector {
297
352
  const timer = setTimeout(() => {
298
353
  this._removeWaiter(waiter);
299
354
  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}`));
355
+ const expected = accept ? [...accept].join(" or ") : "handler.conversation or handler.event";
356
+ const reason = this.streamError ? `SSE stream died: ${this.streamError.message}` : `no ${expected} span completed`;
357
+ reject(
358
+ new Error(
359
+ `waitForTurnComplete timed out after ${opts.timeout}ms \u2014 ${reason}; diagnostics: ${this._formatTurnDiagnostics()}`
360
+ )
361
+ );
302
362
  }, opts.timeout);
303
363
  const onAbort = () => {
304
364
  clearTimeout(timer);
@@ -307,11 +367,11 @@ var SSECollector = class _SSECollector {
307
367
  };
308
368
  opts.abortSignal?.addEventListener("abort", onAbort, { once: true });
309
369
  const waiter = (_span) => {
310
- if (this._hasNewCompletedHandler()) {
370
+ if (this._hasNewCompletedHandler(accept)) {
311
371
  clearTimeout(timer);
312
372
  this._removeWaiter(waiter);
313
373
  opts.abortSignal?.removeEventListener("abort", onAbort);
314
- this._settleAfterHandler().then(resolve2, reject);
374
+ this._settleAfterHandler(settle).then(resolve2, reject);
315
375
  }
316
376
  };
317
377
  this.waiters.push(waiter);
@@ -325,7 +385,9 @@ var SSECollector = class _SSECollector {
325
385
  * Exposes a cancellation hook via _cancelSettle so disconnect() can short-
326
386
  * circuit an in-flight settle phase and avoid leaking timers for up to 2s.
327
387
  */
328
- _settleAfterHandler() {
388
+ _settleAfterHandler(settle) {
389
+ const quietMs = settle?.quietMs ?? _SSECollector.SETTLE_MS;
390
+ const maxMs = settle?.maxMs ?? _SSECollector.MAX_SETTLE_MS;
329
391
  return new Promise((resolve2) => {
330
392
  let resolved = false;
331
393
  const done = () => {
@@ -337,12 +399,12 @@ var SSECollector = class _SSECollector {
337
399
  this._cancelSettle = null;
338
400
  resolve2();
339
401
  };
340
- const hardCap = setTimeout(done, _SSECollector.MAX_SETTLE_MS);
341
- let settleTimer = setTimeout(done, _SSECollector.SETTLE_MS);
402
+ const hardCap = setTimeout(done, maxMs);
403
+ let settleTimer = setTimeout(done, quietMs);
342
404
  const settleWaiter = () => {
343
405
  if (resolved) return;
344
406
  clearTimeout(settleTimer);
345
- settleTimer = setTimeout(done, _SSECollector.SETTLE_MS);
407
+ settleTimer = setTimeout(done, quietMs);
346
408
  };
347
409
  this.waiters.push(settleWaiter);
348
410
  this._cancelSettle = done;
@@ -507,6 +569,7 @@ ${value}` : value;
507
569
  * Dispatch a parsed SSE event to the appropriate handler.
508
570
  */
509
571
  _handleEvent(eventName, payload) {
572
+ this._recordEvent(eventName);
510
573
  if (eventName === "snapshot") {
511
574
  const data2 = payload;
512
575
  const spans = Array.isArray(data2.spans) ? data2.spans : [];
@@ -526,6 +589,44 @@ ${value}` : value;
526
589
  }
527
590
  }
528
591
  }
592
+ _recordEvent(eventName) {
593
+ this.lastEvent = { name: eventName, at: Date.now() };
594
+ if (eventName === "update") {
595
+ this.updateEventCount++;
596
+ } else if (eventName === "keepalive") {
597
+ this.keepaliveEventCount++;
598
+ }
599
+ }
600
+ _formatTurnDiagnostics() {
601
+ const turnSpans = this.getTurnSpans();
602
+ const spanNames = /* @__PURE__ */ new Set();
603
+ const handlerStatuses = /* @__PURE__ */ new Set();
604
+ for (const span of turnSpans) {
605
+ spanNames.add(span.name);
606
+ if (isTurnHandlerSpan(span)) {
607
+ handlerStatuses.add(span.status);
608
+ }
609
+ }
610
+ const lastEvent = this.lastEvent ? `${this.lastEvent.name} ${Date.now() - this.lastEvent.at}ms ago` : "none";
611
+ return [
612
+ `snapshotReceived=${this.snapshotReceived}`,
613
+ `totalUpdates=${this.updateEventCount}`,
614
+ `totalKeepalives=${this.keepaliveEventCount}`,
615
+ `lastEvent=${lastEvent}`,
616
+ `totalSpans=${this.spanMap.size}`,
617
+ `turnSpans=${turnSpans.length}`,
618
+ `turnSpanNames=${this._formatSet(spanNames)}`,
619
+ `turnHandlerStatuses=${this._formatSet(handlerStatuses)}`
620
+ ].join(", ");
621
+ }
622
+ _formatSet(values, limit = 12) {
623
+ if (values.size === 0) {
624
+ return "none";
625
+ }
626
+ const items = Array.from(values);
627
+ const suffix = items.length > limit ? `|+${items.length - limit} more` : "";
628
+ return `${items.slice(0, limit).join("|")}${suffix}`;
629
+ }
529
630
  /**
530
631
  * Insert or replace a span in the map (dedup by trace+span ID).
531
632
  */
@@ -551,7 +652,7 @@ ${value}` : value;
551
652
  }
552
653
  }
553
654
  /**
554
- * Check if there is a handler.conversation span that:
655
+ * Check if there is a turn-handler span (handler.conversation / handler.event) that:
555
656
  * 1. Was NOT seen before startTurn()
556
657
  * 2. Has terminal status (ok or error)
557
658
  *
@@ -560,9 +661,9 @@ ${value}` : value;
560
661
  * waitForTurnComplete check (which would otherwise resolve instantly
561
662
  * against stale data instead of waiting for the new turn's handler).
562
663
  */
563
- _hasNewCompletedHandler() {
664
+ _hasNewCompletedHandler(acceptSpanNames) {
564
665
  for (const [key, span] of this.spanMap) {
565
- if (span.name === "handler.conversation" && !this.seenHandlerKeys.has(key) && (span.status === "ok" || span.status === "error")) {
666
+ if (isTurnHandlerSpan(span) && (!acceptSpanNames || acceptSpanNames.has(span.name)) && !this.seenHandlerKeys.has(key) && (span.status === "ok" || span.status === "error")) {
566
667
  this.seenHandlerKeys.add(key);
567
668
  return true;
568
669
  }
@@ -589,6 +690,38 @@ ${value}` : value;
589
690
  }
590
691
  };
591
692
 
693
+ // src/spans/local-span-source.ts
694
+ var LocalSpanSource = class {
695
+ collector;
696
+ constructor(devServerUrl, headers = {}) {
697
+ this.collector = new SSECollector(devServerUrl, headers);
698
+ }
699
+ async connect(filter) {
700
+ await this.collector.connect(filter);
701
+ }
702
+ async repoint(filter) {
703
+ await this.collector.repoint(filter);
704
+ }
705
+ startTurn() {
706
+ this.collector.startTurn();
707
+ }
708
+ async waitForTurnComplete(opts) {
709
+ await this.collector.waitForTurnComplete(opts);
710
+ }
711
+ async waitForWorkflow(name, opts) {
712
+ await this.collector.waitForWorkflow(name, opts);
713
+ }
714
+ getTurnSpans() {
715
+ return this.collector.getTurnSpans();
716
+ }
717
+ getAllSpans() {
718
+ return this.collector.getAllSpans();
719
+ }
720
+ disconnect() {
721
+ this.collector.disconnect();
722
+ }
723
+ };
724
+
592
725
  // src/transformer.ts
593
726
  function data(span) {
594
727
  return span.data && typeof span.data === "object" ? span.data : {};
@@ -676,7 +809,7 @@ function extractWorkflowSpans(spans) {
676
809
  }
677
810
  function extractHandler(spans) {
678
811
  const handler = spans.find(
679
- (s) => (s.name === "handler.conversation" || s.name === "handler.workflow") && s.status !== "running"
812
+ (s) => (s.name === "handler.conversation" || s.name === "handler.event" || s.name === "handler.workflow") && s.status !== "running"
680
813
  );
681
814
  if (!handler) {
682
815
  return { handlerDuration: 0, handlerStatus: "ok" };
@@ -688,55 +821,26 @@ function extractHandler(spans) {
688
821
  }
689
822
 
690
823
  // src/graders/llm.ts
691
- import { Client } from "@botpress/client";
692
824
  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
- }
825
+ import { Zai } from "@botpress/zai";
718
826
  var MODEL_ALIASES = ["fast", "best"];
719
- var _cognitive = null;
827
+ var _zai = null;
720
828
  var _availableModels = [];
721
829
  var _judgeModel = "fast";
830
+ var _failClosed = true;
722
831
  var _log = defaultLogger;
723
832
  function _isModelValid(model) {
724
833
  if (MODEL_ALIASES.includes(model)) return true;
725
834
  if (_availableModels.length === 0) return false;
726
835
  return _availableModels.includes(model);
727
836
  }
728
- async function initLLMJudge(credentials, options) {
837
+ async function initLLMJudge(client, options) {
729
838
  const log = options?.logger ?? defaultLogger;
730
839
  _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 });
840
+ _failClosed = options?.failClosed ?? true;
841
+ const cognitive = new Cognitive({ client, __experimental_beta: true });
738
842
  try {
739
- _availableModels = Array.from((await _cognitive.fetchRemoteModels()).keys());
843
+ _availableModels = Array.from((await cognitive.fetchRemoteModels()).keys());
740
844
  } catch (err) {
741
845
  log.warn(`Failed to fetch available models: ${err.message}. LLM judge will be unavailable.`);
742
846
  _availableModels = [];
@@ -747,66 +851,42 @@ async function initLLMJudge(credentials, options) {
747
851
  } else if (!_isModelValid(_judgeModel)) {
748
852
  log.warn(`Configured LLM judge model "${_judgeModel}" is invalid. Run "adk models" to list available models.`);
749
853
  }
854
+ _zai = new Zai({ client: cognitive, modelId: _judgeModel });
750
855
  }
751
856
  async function gradeLLMJudge(botResponse, criteria, context) {
752
- const threshold = Math.max(1, Math.min(5, context.passThreshold ?? DEFAULT_PASS_THRESHOLD));
857
+ const assertion = `llm_judge: "${criteria}"`;
753
858
  try {
754
- if (!_cognitive) {
859
+ if (!_zai) {
755
860
  _log.warn(`LLM judge unavailable for "${criteria}": no credentials configured (initLLMJudge not called)`);
756
861
  return {
757
- assertion: `llm_judge: "${criteria}"`,
758
- pass: false,
862
+ assertion,
863
+ pass: !_failClosed,
864
+ skipped: true,
759
865
  expected: criteria,
760
866
  actual: "FAILED \u2014 LLM judge unavailable: no credentials configured"
761
867
  };
762
868
  }
763
869
  if (!_isModelValid(_judgeModel)) {
764
870
  return {
765
- assertion: `llm_judge: "${criteria}"`,
871
+ assertion,
766
872
  pass: false,
767
873
  expected: criteria,
768
874
  actual: "FAILED \u2014 invalid LLM judge model"
769
875
  };
770
876
  }
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);
877
+ const { output } = await _zai.check({ userMessage: context.userMessage, botResponse }, criteria).result();
799
878
  return {
800
- assertion: `llm_judge: "${criteria}"`,
801
- pass: verdict.score >= threshold,
879
+ assertion,
880
+ pass: output.value,
802
881
  expected: criteria,
803
- actual: `Score ${verdict.score}/5 (threshold: ${threshold}) \u2014 ${verdict.reason}`
882
+ actual: `${output.value ? "Pass" : "Fail"} \u2014 ${output.explanation}`
804
883
  };
805
884
  } catch (err) {
806
885
  _log.warn(`LLM judge failed for "${criteria}": ${err.message}`);
807
886
  return {
808
- assertion: `llm_judge: "${criteria}"`,
809
- pass: false,
887
+ assertion,
888
+ pass: !_failClosed,
889
+ skipped: true,
810
890
  expected: criteria,
811
891
  actual: `FAILED \u2014 LLM judge unavailable: ${err.message}`
812
892
  };
@@ -1229,6 +1309,7 @@ function filterEvals(evals, filter) {
1229
1309
  }
1230
1310
 
1231
1311
  // src/runner.ts
1312
+ import { isAdkError } from "@botpress/analytics";
1232
1313
  import { randomUUID } from "crypto";
1233
1314
  var DEFAULT_IDLE_TIMEOUT = 3e4;
1234
1315
  var DEFAULT_DEV_SERVER_URL = "http://localhost:3001";
@@ -1315,27 +1396,22 @@ async function runEval(evalDef, connection, options = {}) {
1315
1396
  const start = Date.now();
1316
1397
  const turns = [];
1317
1398
  let outcomeAssertions = [];
1318
- const collector = new SSECollector(devServerUrl, devServerHeaders);
1399
+ const collector = options.spanSource ?? new LocalSpanSource(devServerUrl, devServerHeaders);
1319
1400
  try {
1320
- const session = new ChatSession(connection.webhookId, options.chatClient);
1401
+ const session = new ChatSession(
1402
+ connection.client,
1403
+ connection.botId,
1404
+ options.chatWebhookId,
1405
+ options.chatBaseUrl,
1406
+ options.chatClient
1407
+ );
1321
1408
  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
1409
  let conversationId = "";
1334
1410
  if (evalDef.setup?.state?.conversation || evalDef.setup?.workflow) {
1335
1411
  conversationId = await session.ensureConversation();
1336
1412
  }
1337
1413
  await seedEvalState(
1338
- getBpClient(),
1414
+ connection.client,
1339
1415
  {
1340
1416
  botId: connection.botId,
1341
1417
  userId: session.userId,
@@ -1344,7 +1420,7 @@ async function runEval(evalDef, connection, options = {}) {
1344
1420
  evalDef.setup
1345
1421
  );
1346
1422
  await triggerEvalWorkflow(
1347
- getBpClient(),
1423
+ connection.client,
1348
1424
  {
1349
1425
  userId: session.userId,
1350
1426
  ...conversationId ? { conversationId } : {}
@@ -1380,24 +1456,34 @@ async function runEval(evalDef, connection, options = {}) {
1380
1456
  if (!turn.user && !turn.event) {
1381
1457
  throw turnConfigError(`must have either 'user' or 'event'.`);
1382
1458
  }
1383
- const turnLabelForEvent = turn.event ? `[event: ${turn.event.type}]` : turn.user;
1384
- options.onProgress?.({
1459
+ const turnLabel = turn.event ? "[event]" : turn.user;
1460
+ await options.onProgress?.({
1385
1461
  type: "turn_start",
1386
1462
  evalName: evalDef.name,
1387
1463
  evalIndex: options.evalIndex ?? 0,
1388
- turnNumber: i + 1,
1464
+ turnIndex: i,
1389
1465
  totalTurns: evalDef.conversation.length,
1390
- userMessage: turnLabelForEvent
1466
+ userMessage: turnLabel
1391
1467
  });
1468
+ if (turn.newConversation && i > 0) {
1469
+ const newConversationId = await session.newConversation();
1470
+ await collector.repoint({ conversationId: newConversationId });
1471
+ }
1392
1472
  collector.startTurn();
1393
1473
  if (turn.event) {
1394
- await session.sendEvent(turn.event.type, turn.event.payload ?? {});
1474
+ await session.sendEvent(turn.event.payload);
1395
1475
  } else {
1396
1476
  await session.sendMessage(turn.user);
1397
1477
  }
1398
1478
  try {
1399
1479
  await collector.waitForTurnComplete({
1400
1480
  timeout: idleTimeout,
1481
+ acceptSpanNames: turn.event ? /* @__PURE__ */ new Set(["handler.event"]) : /* @__PURE__ */ new Set(["handler.conversation"]),
1482
+ // Long quiet window: a tool-started workflow and its callback only run
1483
+ // AFTER the handler closes; sending the next message into that window
1484
+ // can get it absorbed by their transcript saves and silently skipped.
1485
+ settleQuietMs: 1500,
1486
+ settleMaxMs: 15e3,
1401
1487
  ...options.signal !== void 0 ? { abortSignal: options.signal } : {}
1402
1488
  });
1403
1489
  } catch (err) {
@@ -1421,7 +1507,6 @@ async function runEval(evalDef, connection, options = {}) {
1421
1507
  const turnData = transformSpans(turnSpans);
1422
1508
  const botResponse = turnData.messages.join("\n");
1423
1509
  const botDuration = turnData.handlerDuration;
1424
- const turnLabel = turn.event ? `[event: ${turn.event.type}]` : turn.user;
1425
1510
  const evalStart = Date.now();
1426
1511
  let assertions = [];
1427
1512
  const noResponse = !turn.expectSilence && turnData.messages.length === 0;
@@ -1476,7 +1561,7 @@ async function runEval(evalDef, connection, options = {}) {
1476
1561
  const turnPass = assertions.every((a) => a.pass);
1477
1562
  const evalDuration = Date.now() - evalStart;
1478
1563
  const turnReport = {
1479
- turnNumber: i + 1,
1564
+ turnIndex: i,
1480
1565
  userMessage: turnLabel,
1481
1566
  botResponse,
1482
1567
  assertions,
@@ -1485,11 +1570,11 @@ async function runEval(evalDef, connection, options = {}) {
1485
1570
  evalDuration
1486
1571
  };
1487
1572
  turns.push(turnReport);
1488
- options.onProgress?.({
1573
+ await options.onProgress?.({
1489
1574
  type: "turn_complete",
1490
1575
  evalName: evalDef.name,
1491
1576
  evalIndex: options.evalIndex ?? 0,
1492
- turnNumber: i + 1,
1577
+ turnIndex: i,
1493
1578
  totalTurns: evalDef.conversation.length,
1494
1579
  turnReport
1495
1580
  });
@@ -1536,7 +1621,15 @@ async function runEval(evalDef, connection, options = {}) {
1536
1621
  } catch (err) {
1537
1622
  const adkErr = isAdkError(err) ? err : void 0;
1538
1623
  if (!adkErr || !adkErr.expected) {
1539
- logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${err.message}`);
1624
+ const errMsg = err.message ?? String(err);
1625
+ const errStack = err.stack ?? "";
1626
+ logger.error(`Eval "${evalDef.name}" failed unexpectedly: ${errMsg}`);
1627
+ logger.error(` stack: ${errStack}`);
1628
+ logger.error(` eval: ${evalDef.name}, turn: ${turns.length}/${evalDef.conversation.length}`);
1629
+ logger.error(` connection: botId=${connection.botId}`);
1630
+ logger.error(
1631
+ ` spanSource: ${options.spanSource?.constructor?.name ?? "default"}, devServerUrl: ${options.devServerUrl ?? "n/a"}`
1632
+ );
1540
1633
  options.onException?.(err, { source: "evals", eval_name: evalDef.name });
1541
1634
  }
1542
1635
  return {
@@ -1554,9 +1647,15 @@ async function runEval(evalDef, connection, options = {}) {
1554
1647
  }
1555
1648
  async function runEvalSuite(config, filter) {
1556
1649
  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);
1650
+ const runId = config.runId ?? randomUUID().replace(/-/g, "").slice(0, 26);
1651
+ let allEvals;
1652
+ if (config.definitions) {
1653
+ allEvals = config.definitions;
1654
+ } else if (config.agentPath) {
1655
+ allEvals = await loadEvalsFromDir(`${config.agentPath}/evals`);
1656
+ } else {
1657
+ throw new Error("Either `definitions` or `agentPath` must be provided in EvalRunnerConfig");
1658
+ }
1560
1659
  const evals = filterEvals(allEvals, filter);
1561
1660
  if (evals.length === 0) {
1562
1661
  return {
@@ -1570,37 +1669,26 @@ async function runEvalSuite(config, filter) {
1570
1669
  ...filter !== void 0 ? { filter } : {}
1571
1670
  };
1572
1671
  }
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
- }
1672
+ await initLLMJudge(config.client, {
1673
+ ...config.evalOptions?.judgeModel !== void 0 ? { model: config.evalOptions.judgeModel } : {},
1674
+ ...config.logger !== void 0 ? { logger: config.logger } : {}
1675
+ });
1589
1676
  const connection = {
1590
- webhookId,
1591
- botId: config.credentials.botId,
1592
- token: config.credentials.token,
1593
- apiUrl: config.credentials.apiUrl
1677
+ client: config.client,
1678
+ botId: config.botId
1594
1679
  };
1595
1680
  const devServerUrl = config.devServerUrl || DEFAULT_DEV_SERVER_URL;
1596
1681
  const devServerHeaders = config.devServerHeaders ?? {};
1597
- await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
1682
+ if (!config.createSpanSource) {
1683
+ await assertChatChannelBound(devServerUrl, devServerHeaders, config.logger);
1684
+ await assertTraceStreamReadable(devServerUrl, devServerHeaders);
1685
+ }
1598
1686
  const reports = [];
1599
- config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
1687
+ await config.onProgress?.({ type: "suite_start", totalEvals: evals.length });
1600
1688
  for (let i = 0; i < evals.length; i++) {
1601
1689
  if (config.signal?.aborted) break;
1602
1690
  const evalDef = evals[i];
1603
- config.onProgress?.({
1691
+ await config.onProgress?.({
1604
1692
  type: "eval_start",
1605
1693
  evalName: evalDef.name,
1606
1694
  index: i,
@@ -1616,10 +1704,13 @@ async function runEvalSuite(config, filter) {
1616
1704
  ...config.signal !== void 0 ? { signal: config.signal } : {},
1617
1705
  ...config.chatClient !== void 0 ? { chatClient: config.chatClient } : {},
1618
1706
  ...config.logger !== void 0 ? { logger: config.logger } : {},
1619
- ...config.onException !== void 0 ? { onException: config.onException } : {}
1707
+ ...config.onException !== void 0 ? { onException: config.onException } : {},
1708
+ ...config.createSpanSource !== void 0 ? { spanSource: config.createSpanSource() } : {},
1709
+ ...config.chatWebhookId !== void 0 ? { chatWebhookId: config.chatWebhookId } : {},
1710
+ ...config.chatBaseUrl !== void 0 ? { chatBaseUrl: config.chatBaseUrl } : {}
1620
1711
  });
1621
1712
  reports.push(report);
1622
- config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
1713
+ await config.onProgress?.({ type: "eval_complete", evalName: evalDef.name, index: i, report });
1623
1714
  }
1624
1715
  const runReport = {
1625
1716
  id: runId,
@@ -1634,7 +1725,7 @@ async function runEvalSuite(config, filter) {
1634
1725
  if (config.signal?.aborted) {
1635
1726
  runReport.aborted = true;
1636
1727
  }
1637
- config.onProgress?.({ type: "suite_complete", report: runReport });
1728
+ await config.onProgress?.({ type: "suite_complete", report: runReport });
1638
1729
  return runReport;
1639
1730
  }
1640
1731
  export {