@burtson-labs/bandit-engine 2.0.71 → 2.0.73

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.js CHANGED
@@ -21021,6 +21021,7 @@ ${protocol}`;
21021
21021
  let latestDisplayMessage = "";
21022
21022
  let sawToolBlock = false;
21023
21023
  const nativeToolCalls = [];
21024
+ let workingTimer = null;
21024
21025
  const stripThinking = (text) => {
21025
21026
  let result = text.replace(/<think>[\s\S]*?<\/think>/g, "");
21026
21027
  const openIdx = result.indexOf("<think>");
@@ -21028,6 +21029,28 @@ ${protocol}`;
21028
21029
  return result.trimStart();
21029
21030
  };
21030
21031
  const stripToolBlocks = (text) => text.replace(/```(?:tool_code|TOOL_CODE)\s*\n[\s\S]*?\n```/gi, "").trim();
21032
+ let workingLabel = "Working on it";
21033
+ let workingPreamble = "";
21034
+ const stopWorking = () => {
21035
+ if (workingTimer) {
21036
+ clearInterval(workingTimer);
21037
+ workingTimer = null;
21038
+ }
21039
+ };
21040
+ const startWorking = (label = "Working on it") => {
21041
+ workingLabel = label.replace(/[.…]+\s*$/, "").trim() || "Working on it";
21042
+ workingPreamble = stripToolBlocks(stripThinking(fullMessage)).trim();
21043
+ if (workingTimer) return;
21044
+ let dots = 0;
21045
+ const render = () => {
21046
+ dots = (dots + 1) % 4;
21047
+ setStreamBuffer(
21048
+ `${workingPreamble}${workingPreamble ? "\n\n" : ""}_${workingLabel}${".".repeat(dots)}_`
21049
+ );
21050
+ };
21051
+ render();
21052
+ workingTimer = setInterval(render, 450);
21053
+ };
21031
21054
  const flushNow = () => {
21032
21055
  clearFlushTimer();
21033
21056
  if (!sawToolBlock) {
@@ -21063,6 +21086,7 @@ ${protocol}`;
21063
21086
  nativeToolCalls.push(...data.message.tool_calls);
21064
21087
  sawToolBlock = true;
21065
21088
  clearFlushTimer();
21089
+ startWorking();
21066
21090
  }
21067
21091
  if (data.message.content) {
21068
21092
  fullMessage += data.message.content;
@@ -21074,6 +21098,7 @@ ${protocol}`;
21074
21098
  if (/```(?:tool_code|TOOL_CODE)/.test(visibleMessage)) {
21075
21099
  sawToolBlock = true;
21076
21100
  clearFlushTimer();
21101
+ startWorking();
21077
21102
  }
21078
21103
  latestDisplayMessage = visibleMessage;
21079
21104
  if (!sawToolBlock) {
@@ -21082,6 +21107,7 @@ ${protocol}`;
21082
21107
  },
21083
21108
  error: (err) => {
21084
21109
  debugLogger.error("Stream error:", err);
21110
+ stopWorking();
21085
21111
  overrideComponentStatus("Idle");
21086
21112
  setIsSubmitting(false);
21087
21113
  setIsStreaming(false);
@@ -21166,6 +21192,7 @@ ${fn}(${argStr})
21166
21192
  if (functionName === "ask_user" || functionName === "ask-user") {
21167
21193
  enhancedMessage = enhancedMessage.replace(match, "");
21168
21194
  clearFlushTimer();
21195
+ stopWorking();
21169
21196
  const askPreamble = stripToolBlocks(fullMessage).trim();
21170
21197
  setStreamBuffer(askPreamble || "_Waiting for your answer\u2026_");
21171
21198
  const questions = parseAskUserQuestions(
@@ -21188,11 +21215,8 @@ A: ${(answers[q.id] || "").trim() || "(no answer)"}`).join("\n\n") : "The user d
21188
21215
  const placeholderToken = `<<TOOL_LOADING_${functionName}_${Math.random().toString(36).slice(2)}>>`;
21189
21216
  enhancedMessage = enhancedMessage.replace(match, placeholderToken);
21190
21217
  clearFlushTimer();
21191
- const toolStatus = functionName === "web_search" || functionName === "web-search" ? "Searching the web\u2026" : functionName === "web_fetch" || functionName === "web-fetch" ? "Reading the page\u2026" : functionName === "image_generation" || functionName === "image-generation" ? "Generating the image\u2026" : "Working on it\u2026";
21192
- const toolPreamble = stripToolBlocks(fullMessage).trim();
21193
- setStreamBuffer(toolPreamble ? `${toolPreamble}
21194
-
21195
- _${toolStatus}_` : `_${toolStatus}_`);
21218
+ const toolStatus = functionName === "web_search" || functionName === "web-search" ? "Searching the web" : functionName === "web_fetch" || functionName === "web-fetch" ? "Reading the page" : functionName === "image_generation" || functionName === "image-generation" ? "Generating the image" : functionName === "create_file" || functionName === "create-file" ? "Creating your file" : "Working on it";
21219
+ startWorking(toolStatus);
21196
21220
  telemetryEvent("tool_loop:tool_execute", { name: functionName, params: parsedParams });
21197
21221
  const result = await executeMCPTool({
21198
21222
  toolName: functionName,
@@ -21306,71 +21330,170 @@ _This link is temporary and expires in about ${mins} minutes._`;
21306
21330
  try {
21307
21331
  const toolResultsText = summarizableResults.map((r) => `## ${r.name}
21308
21332
  ${r.output}`).join("\n\n");
21309
- const summaryMessages = [
21310
- { role: "system", content: systemPromptForSummary },
21333
+ const MAX_CHAIN_ROUNDS = 4;
21334
+ const enabledToolsForChain = getEnabledMCPToolsForAI();
21335
+ const convo = [
21336
+ { role: "system", content: enhancedSystemPrompt },
21311
21337
  ...contextMessages,
21312
21338
  { role: "user", content: question },
21313
- { role: "assistant", content: stripToolBlocks(fullMessage) || "Let me look that up." },
21339
+ { role: "assistant", content: stripToolBlocks(fullMessage) || "Let me work on that." },
21314
21340
  {
21315
21341
  role: "user",
21316
- content: `I ran the tool(s) you requested. Here are the raw results:
21342
+ content: `Here are the results of the tool(s) so far:
21317
21343
 
21318
21344
  ${toolResultsText}
21319
21345
 
21320
- Using these results together with your own knowledge, answer my original question concisely and in a natural, well-formatted way. Do NOT add a "Sources", "References", or "Citations" list of any kind \u2014 not names and not URLs. A clean, clickable Sources section is appended automatically below your answer, so any list you add just duplicates it. (Mentioning a source naturally inside a sentence is fine; a trailing list is not.) Do NOT output tool_code or call any tools again.`
21346
+ Use them to fully complete my original request. If you still need to take an action I asked for (for example, actually create a file I want to download), call the appropriate tool now with a \`\`\`tool_code\`\`\` block. Otherwise give your final answer. Do NOT add a "Sources"/"References"/"Citations" list \u2014 one is appended automatically.`
21321
21347
  }
21322
21348
  ];
21323
- const summaryRequest = {
21324
- model: modelName,
21325
- messages: summaryMessages,
21326
- stream: true,
21327
- options: { num_predict: tokenLimit + 250 }
21328
- };
21329
- clearFlushTimer();
21330
- setStreamBuffer("");
21331
- setIsThinking?.(true);
21332
- const summaryText = await new Promise((resolve) => {
21349
+ const streamTurn = (req) => new Promise((resolve) => {
21333
21350
  let acc = "";
21351
+ const native = [];
21334
21352
  let settled = false;
21335
21353
  let timer;
21336
- const done = (value) => {
21354
+ const finish = (value) => {
21337
21355
  if (settled) return;
21338
21356
  settled = true;
21339
21357
  if (timer) clearTimeout(timer);
21340
21358
  resolve(value);
21341
21359
  };
21342
- const summarySub = provider.chat(summaryRequest).subscribe({
21360
+ const sub2 = provider.chat(req).subscribe({
21343
21361
  next: (data) => {
21362
+ if (Array.isArray(data?.message?.tool_calls) && data.message.tool_calls.length) {
21363
+ native.push(...data.message.tool_calls);
21364
+ }
21344
21365
  if (data?.message?.content) {
21345
21366
  acc += data.message.content;
21346
21367
  const visible = stripThinking(acc);
21347
21368
  latestDisplayMessage = visible;
21348
21369
  lastPartialRef.current.text = visible;
21349
- if (visible) setIsThinking?.(false);
21370
+ if (visible) {
21371
+ stopWorking();
21372
+ setIsThinking?.(false);
21373
+ }
21350
21374
  setStreamBuffer(visible);
21351
21375
  }
21352
21376
  },
21353
- error: (summaryErr) => {
21354
- debugLogger.error("Summarization pass failed", {
21355
- error: summaryErr instanceof Error ? summaryErr.message : String(summaryErr)
21356
- });
21357
- done("");
21358
- },
21359
- complete: () => done(stripThinking(acc).trim())
21377
+ error: () => finish({ text: stripThinking(acc).trim(), native }),
21378
+ complete: () => finish({ text: stripThinking(acc).trim(), native })
21360
21379
  });
21361
- currentSubRef.current = summarySub;
21380
+ currentSubRef.current = sub2;
21362
21381
  timer = setTimeout(() => {
21363
- debugLogger.warn("Summarization pass timed out; using inline tool output");
21364
21382
  try {
21365
- summarySub.unsubscribe();
21383
+ sub2.unsubscribe();
21366
21384
  } catch {
21367
21385
  }
21368
- done("");
21386
+ finish({ text: stripThinking(acc).trim(), native });
21369
21387
  }, 3e4);
21370
21388
  });
21389
+ const runChainedTool = async (fn, params) => {
21390
+ if (fn === "ask_user" || fn === "ask-user") {
21391
+ const qs = parseAskUserQuestions(params.questions ?? params);
21392
+ if (!qs.length) return "ask_user failed: it needs a questions array.";
21393
+ const ans = await useAskUserStore.getState().ask(qs);
21394
+ return ans ? "The user answered:\n\n" + qs.map((q) => `Q: ${q.question}
21395
+ A: ${(ans[q.id] || "").trim() || "(no answer)"}`).join("\n\n") : "The user dismissed the question(s). Proceed with your best judgment.";
21396
+ }
21397
+ const status = fn === "create_file" || fn === "create-file" ? "Creating your file" : fn === "web_search" || fn === "web-search" ? "Searching the web" : fn === "web_fetch" || fn === "web-fetch" ? "Reading the page" : fn === "image_generation" || fn === "image-generation" ? "Generating the image" : "Working on it";
21398
+ startWorking(status);
21399
+ const result = await executeMCPTool({ toolName: fn, parameters: params });
21400
+ if (!result.success) return `That step failed: ${result.error || "unknown error"}.`;
21401
+ if (fn === "create_file" || fn === "create-file") {
21402
+ const f = result.data ?? {};
21403
+ if (f.url) {
21404
+ const mins = f.expiresInMinutes ?? 60;
21405
+ const name = f.filename || "your file";
21406
+ inlineImageBlocks.push(`\u{1F4C4} **[${name}](${f.url})** \u2014 ready to download.
21407
+
21408
+ _This link is temporary and expires in about ${mins} minutes._`);
21409
+ return `File created and its download link is now shown to the user. Briefly confirm it's ready and that it expires in ~${mins} minutes.`;
21410
+ }
21411
+ return "The file was created.";
21412
+ }
21413
+ if (fn === "image_generation" || fn === "image-generation") {
21414
+ const img = result.data ?? {};
21415
+ if (img.imageUrl) {
21416
+ inlineImageBlocks.push(`![Generated image](${img.imageUrl})`);
21417
+ return "Image generated and shown to the user.";
21418
+ }
21419
+ }
21420
+ if (typeof result.data === "string") return result.data.slice(0, 2e3);
21421
+ if (result.data) return JSON.stringify(result.data).slice(0, 1500);
21422
+ return "Done.";
21423
+ };
21424
+ clearFlushTimer();
21425
+ let finalText = "";
21426
+ let lastTurnText = "";
21427
+ for (let round = 0; round < MAX_CHAIN_ROUNDS; round++) {
21428
+ stopWorking();
21429
+ setStreamBuffer("");
21430
+ setIsThinking?.(true);
21431
+ const turnRequest = {
21432
+ model: modelName,
21433
+ messages: convo,
21434
+ stream: true,
21435
+ tools: enabledToolsForChain.length ? enabledToolsForChain : void 0,
21436
+ options: { num_predict: tokenLimit + 250 }
21437
+ };
21438
+ const { text: turnText, native: turnNative } = await streamTurn(turnRequest);
21439
+ setIsThinking?.(false);
21440
+ if (turnText.trim()) lastTurnText = turnText;
21441
+ let toolText = turnText;
21442
+ if (turnNative.length && !/```(?:tool_code|TOOL_CODE)/.test(toolText)) {
21443
+ for (const raw of turnNative) {
21444
+ const tc = raw;
21445
+ const fnName = tc.function?.name ?? tc.name;
21446
+ if (!fnName) continue;
21447
+ const a = tc.function?.arguments ?? tc.arguments ?? {};
21448
+ toolText += `
21449
+
21450
+ \`\`\`tool_code
21451
+ ${fnName}(${typeof a === "string" ? a : JSON.stringify(a ?? {})})
21452
+ \`\`\``;
21453
+ }
21454
+ }
21455
+ const chainMatches = toolText.match(/```(?:tool_code|TOOL_CODE)\s*\n([^`]+)\n```/gi);
21456
+ if (!chainMatches || !chainMatches.length) {
21457
+ finalText = turnText;
21458
+ break;
21459
+ }
21460
+ const roundOut = [];
21461
+ for (const m of chainMatches) {
21462
+ const code = m.replace(/```(?:tool_code|TOOL_CODE)\s*\n|\n```/gi, "").trim();
21463
+ const fm = code.match(/^(\w+)\(\s*(.*?)\s*\)$/);
21464
+ if (!fm) continue;
21465
+ const [, fnName, rawParams] = fm;
21466
+ let parsed = {};
21467
+ const rp = rawParams.trim();
21468
+ if (rp) {
21469
+ try {
21470
+ parsed = JSON.parse(rp.startsWith("{") ? rp : `{${rp}}`);
21471
+ } catch {
21472
+ parsed = {};
21473
+ }
21474
+ }
21475
+ try {
21476
+ roundOut.push(`## ${fnName}
21477
+ ${await runChainedTool(fnName, parsed)}`);
21478
+ } catch (e) {
21479
+ roundOut.push(`## ${fnName}
21480
+ That step failed: ${e instanceof Error ? e.message : String(e)}`);
21481
+ }
21482
+ }
21483
+ convo.push({ role: "assistant", content: stripToolBlocks(turnText) || "(using a tool)" });
21484
+ convo.push({
21485
+ role: "user",
21486
+ content: `Tool results:
21487
+
21488
+ ${roundOut.join("\n\n")}
21489
+
21490
+ Now give your final answer to my original request, or call another tool if you still genuinely need to. Do NOT add a "Sources" list.`
21491
+ });
21492
+ }
21371
21493
  setIsThinking?.(false);
21372
- if (summaryText.trim()) {
21373
- const cleanedSummary = summaryText.replace(
21494
+ const answerText = finalText.trim() ? finalText : lastTurnText;
21495
+ if (answerText.trim() || inlineImageBlocks.length) {
21496
+ const cleanedSummary = answerText.replace(
21374
21497
  /\n{1,}\s*(?:[*_#>\s]*)(?:sources?|references?|citations?|further reading)(?:\s*:)?\s*(?:[*_]*)\s*\n[\s\S]*$/i,
21375
21498
  ""
21376
21499
  ).trimEnd();
@@ -21396,6 +21519,7 @@ ${inlineImageBlocks.join("\n\n")}` : "");
21396
21519
  }
21397
21520
  }
21398
21521
  }
21522
+ stopWorking();
21399
21523
  overrideComponentStatus("Idle");
21400
21524
  setIsSubmitting(false);
21401
21525
  setPreviousQuestion(question);