bashkit 0.5.0 → 0.5.2

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
@@ -613,14 +613,15 @@ var DEFAULT_CONFIG = {
613
613
  };
614
614
 
615
615
  // src/utils/budget-tracking.ts
616
- var openRouterCache = null;
616
+ var openRouterPricingCache = null;
617
+ var openRouterModelsCache = null;
617
618
  var openRouterCacheTimestamp = 0;
618
619
  var openRouterFetchPromise = null;
619
620
  var OPENROUTER_FETCH_TIMEOUT_MS = 1e4;
620
621
  var OPENROUTER_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
621
- async function fetchOpenRouterPricing(apiKey) {
622
- if (openRouterCache && Date.now() - openRouterCacheTimestamp < OPENROUTER_CACHE_TTL_MS) {
623
- return openRouterCache;
622
+ async function fetchOpenRouterData(apiKey) {
623
+ if (openRouterModelsCache && Date.now() - openRouterCacheTimestamp < OPENROUTER_CACHE_TTL_MS) {
624
+ return openRouterModelsCache;
624
625
  }
625
626
  if (openRouterFetchPromise)
626
627
  return openRouterFetchPromise;
@@ -653,7 +654,9 @@ async function fetchOpenRouterPricing(apiKey) {
653
654
  throw new Error(`[bashkit] OpenRouter pricing response missing data array. ` + `The API may have changed. Please provide modelPricing overrides in your config.`);
654
655
  }
655
656
  const MAX_MODELS = 1e4;
656
- const map = new Map;
657
+ const MAX_CONTEXT_LENGTH = 1e7;
658
+ const modelsMap = new Map;
659
+ const pricingMap = new Map;
657
660
  for (const model of models.slice(0, MAX_MODELS)) {
658
661
  if (!model.id || !model.pricing)
659
662
  continue;
@@ -673,11 +676,19 @@ async function fetchOpenRouterPricing(apiKey) {
673
676
  const cacheWrite = parseFloat(model.pricing.input_cache_write ?? "");
674
677
  if (Number.isFinite(cacheWrite) && cacheWrite >= 0)
675
678
  pricing.cacheWritePerToken = cacheWrite;
676
- map.set(model.id.toLowerCase(), pricing);
679
+ const key = model.id.toLowerCase();
680
+ pricingMap.set(key, pricing);
681
+ const contextLength = model.context_length;
682
+ if (typeof contextLength === "number" && Number.isFinite(contextLength) && contextLength > 0 && contextLength <= MAX_CONTEXT_LENGTH) {
683
+ modelsMap.set(key, { pricing, contextLength });
684
+ } else {
685
+ modelsMap.set(key, { pricing, contextLength: 0 });
686
+ }
677
687
  }
678
- openRouterCache = map;
688
+ openRouterModelsCache = modelsMap;
689
+ openRouterPricingCache = pricingMap;
679
690
  openRouterCacheTimestamp = Date.now();
680
- return openRouterCache;
691
+ return openRouterModelsCache;
681
692
  } finally {
682
693
  clearTimeout(timeoutId);
683
694
  openRouterFetchPromise = null;
@@ -685,6 +696,9 @@ async function fetchOpenRouterPricing(apiKey) {
685
696
  })();
686
697
  return openRouterFetchPromise;
687
698
  }
699
+ async function fetchOpenRouterModels(apiKey) {
700
+ return fetchOpenRouterData(apiKey);
701
+ }
688
702
  function getModelMatchVariants(model) {
689
703
  const lower = model.toLowerCase();
690
704
  const kebab = lower.replace(/[^a-z0-9]+/g, "-");
@@ -700,54 +714,65 @@ function getModelMatchVariants(model) {
700
714
  }
701
715
  return variants;
702
716
  }
703
- function searchModelInCosts(model, costsMap) {
704
- if (costsMap.size === 0)
717
+ function searchModelInMap(model, map, filter) {
718
+ if (map.size === 0)
705
719
  return;
706
720
  const modelVariants = getModelMatchVariants(model);
707
- const costVariantsCache = new Map;
708
- function getCostVariants(key) {
709
- let variants = costVariantsCache.get(key);
721
+ const entryVariantsCache = new Map;
722
+ function getEntryVariants(key) {
723
+ let variants = entryVariantsCache.get(key);
710
724
  if (!variants) {
711
725
  variants = getModelMatchVariants(key);
712
- costVariantsCache.set(key, variants);
726
+ entryVariantsCache.set(key, variants);
713
727
  }
714
728
  return variants;
715
729
  }
716
730
  for (const variant of modelVariants) {
717
- const pricing = costsMap.get(variant);
718
- if (pricing)
719
- return pricing;
731
+ const value = map.get(variant);
732
+ if (value !== undefined && (!filter || filter(value)))
733
+ return value;
720
734
  }
721
735
  let bestMatch;
722
736
  let bestMatchLength = 0;
723
- for (const [costKey, pricing] of costsMap) {
724
- const costVariants = getCostVariants(costKey);
737
+ for (const [entryKey, value] of map) {
738
+ if (filter && !filter(value))
739
+ continue;
740
+ const entryVariants = getEntryVariants(entryKey);
725
741
  for (const modelVariant of modelVariants) {
726
- for (const costVariant of costVariants) {
727
- if (modelVariant.includes(costVariant) && costVariant.length > bestMatchLength) {
728
- bestMatch = pricing;
729
- bestMatchLength = costVariant.length;
742
+ for (const entryVariant of entryVariants) {
743
+ if (modelVariant.includes(entryVariant) && entryVariant.length > bestMatchLength) {
744
+ bestMatch = value;
745
+ bestMatchLength = entryVariant.length;
730
746
  }
731
747
  }
732
748
  }
733
749
  }
734
- if (bestMatch)
750
+ if (bestMatch !== undefined)
735
751
  return bestMatch;
736
752
  let reverseMatch;
737
753
  let reverseMatchLength = Infinity;
738
- for (const [costKey, pricing] of costsMap) {
739
- const costVariants = getCostVariants(costKey);
754
+ for (const [entryKey, value] of map) {
755
+ if (filter && !filter(value))
756
+ continue;
757
+ const entryVariants = getEntryVariants(entryKey);
740
758
  for (const modelVariant of modelVariants) {
741
- for (const costVariant of costVariants) {
742
- if (costVariant.includes(modelVariant) && costVariant.length < reverseMatchLength) {
743
- reverseMatch = pricing;
744
- reverseMatchLength = costVariant.length;
759
+ for (const entryVariant of entryVariants) {
760
+ if (entryVariant.includes(modelVariant) && entryVariant.length < reverseMatchLength) {
761
+ reverseMatch = value;
762
+ reverseMatchLength = entryVariant.length;
745
763
  }
746
764
  }
747
765
  }
748
766
  }
749
767
  return reverseMatch;
750
768
  }
769
+ function searchModelInCosts(model, costsMap) {
770
+ return searchModelInMap(model, costsMap);
771
+ }
772
+ function getModelContextLength(model, modelsMap) {
773
+ const info = searchModelInMap(model, modelsMap, (v) => v.contextLength > 0);
774
+ return info?.contextLength;
775
+ }
751
776
  function findPricingForModel(model, options) {
752
777
  if (options?.overrides) {
753
778
  const overrideMap = options.overrides instanceof Map ? options.overrides : new Map(Object.entries(options.overrides).map(([k, v]) => [
@@ -853,13 +878,14 @@ import { tool, zodSchema } from "ai";
853
878
  import { z } from "zod";
854
879
 
855
880
  // src/utils/debug.ts
881
+ import { AsyncLocalStorage } from "node:async_hooks";
856
882
  import { appendFileSync } from "node:fs";
857
883
  var state = {
858
884
  mode: "off",
859
885
  logs: [],
860
- counters: new Map,
861
- parentStack: []
886
+ counters: new Map
862
887
  };
888
+ var debugContext = new AsyncLocalStorage;
863
889
  var MAX_STRING_LENGTH = 1000;
864
890
  var MAX_ARRAY_ITEMS = 10;
865
891
  function initDebugMode() {
@@ -947,7 +973,8 @@ function emitEvent(event) {
947
973
  }
948
974
  }
949
975
  function formatHumanReadable(event) {
950
- const indent = " ".repeat(state.parentStack.length);
976
+ const ctx = debugContext.getStore();
977
+ const indent = " ".repeat(ctx?.depth ?? 0);
951
978
  if (event.event === "start") {
952
979
  const inputSummary = event.input ? Object.entries(event.input).map(([k, v]) => `${k}=${JSON.stringify(v)}`).slice(0, 3).join(" ") : "";
953
980
  process.stderr.write(`${indent}[bashkit:${event.tool}] → ${inputSummary}
@@ -965,7 +992,8 @@ function debugStart(tool, input) {
965
992
  if (state.mode === "off")
966
993
  return "";
967
994
  const id = generateId(tool);
968
- const parent = state.parentStack.length > 0 ? state.parentStack[state.parentStack.length - 1] : undefined;
995
+ const ctx = debugContext.getStore();
996
+ const parent = ctx?.parentId;
969
997
  const event = {
970
998
  id,
971
999
  ts: Date.now(),
@@ -1003,15 +1031,12 @@ function debugError(id, tool, error) {
1003
1031
  };
1004
1032
  emitEvent(event);
1005
1033
  }
1006
- function pushParent(id) {
1007
- if (state.mode === "off" || !id)
1008
- return;
1009
- state.parentStack.push(id);
1010
- }
1011
- function popParent() {
1012
- if (state.mode === "off")
1013
- return;
1014
- state.parentStack.pop();
1034
+ function runWithDebugParent(parentId, fn) {
1035
+ if (state.mode === "off" || !parentId)
1036
+ return fn();
1037
+ const current = debugContext.getStore();
1038
+ const depth = current ? current.depth + 1 : 1;
1039
+ return debugContext.run({ parentId, depth }, fn);
1015
1040
  }
1016
1041
  function getDebugLogs() {
1017
1042
  return [...state.logs];
@@ -1019,12 +1044,10 @@ function getDebugLogs() {
1019
1044
  function clearDebugLogs() {
1020
1045
  state.logs = [];
1021
1046
  state.counters.clear();
1022
- state.parentStack = [];
1023
1047
  }
1024
1048
  function reinitDebugMode() {
1025
1049
  state.logs = [];
1026
1050
  state.counters.clear();
1027
- state.parentStack = [];
1028
1051
  initDebugMode();
1029
1052
  }
1030
1053
 
@@ -1164,6 +1187,37 @@ function createAskUserTool(config) {
1164
1187
  // src/tools/bash.ts
1165
1188
  import { tool as tool2, zodSchema as zodSchema2 } from "ai";
1166
1189
  import { z as z2 } from "zod";
1190
+
1191
+ // src/utils/helpers.ts
1192
+ function isToolCallPart(part) {
1193
+ return typeof part === "object" && part !== null && part.type === "tool-call" && "toolName" in part && "input" in part;
1194
+ }
1195
+ function isToolResultPart(part) {
1196
+ return typeof part === "object" && part !== null && part.type === "tool-result" && "toolName" in part && "output" in part;
1197
+ }
1198
+ function middleTruncate(text, maxLength) {
1199
+ if (!Number.isFinite(maxLength) || maxLength < 0)
1200
+ return text;
1201
+ if (text.length <= maxLength)
1202
+ return text;
1203
+ const headLength = Math.floor(maxLength / 2);
1204
+ const tailLength = maxLength - headLength;
1205
+ const omitted = text.length - headLength - tailLength;
1206
+ let totalLines = 1;
1207
+ for (let i = 0;i < text.length; i++) {
1208
+ if (text.charCodeAt(i) === 10)
1209
+ totalLines++;
1210
+ }
1211
+ return `[Total output lines: ${totalLines}]
1212
+
1213
+ ` + text.slice(0, headLength) + `
1214
+
1215
+ …${omitted} chars truncated…
1216
+
1217
+ ` + text.slice(text.length - tailLength);
1218
+ }
1219
+
1220
+ // src/tools/bash.ts
1167
1221
  var bashInputSchema = z2.object({
1168
1222
  command: z2.string().describe("The command to execute"),
1169
1223
  timeout: z2.number().nullable().default(null).describe("Optional timeout in milliseconds (max 600000)"),
@@ -1239,16 +1293,8 @@ function createBashTool(sandbox, config) {
1239
1293
  const result = await sandbox.exec(command, {
1240
1294
  timeout: effectiveTimeout
1241
1295
  });
1242
- let stdout = result.stdout;
1243
- let stderr = result.stderr;
1244
- if (stdout.length > maxOutputLength) {
1245
- stdout = stdout.slice(0, maxOutputLength) + `
1246
- [output truncated, ${stdout.length - maxOutputLength} chars omitted]`;
1247
- }
1248
- if (stderr.length > maxOutputLength) {
1249
- stderr = stderr.slice(0, maxOutputLength) + `
1250
- [output truncated, ${stderr.length - maxOutputLength} chars omitted]`;
1251
- }
1296
+ const stdout = middleTruncate(result.stdout, maxOutputLength);
1297
+ const stderr = middleTruncate(result.stderr, maxOutputLength);
1252
1298
  const durationMs = Math.round(performance.now() - startTime);
1253
1299
  if (debugId) {
1254
1300
  debugEnd(debugId, "bash", {
@@ -2598,53 +2644,98 @@ function createTaskTool(config) {
2598
2644
  ...Object.keys(typeConfig.additionalTools ?? {})
2599
2645
  ]
2600
2646
  }) : "";
2601
- if (debugId)
2602
- pushParent(debugId);
2603
- try {
2604
- const model = typeConfig.model || defaultModel;
2605
- const tools = filterTools(allTools, customTools ?? typeConfig.tools, typeConfig.additionalTools);
2606
- const systemPrompt = system_prompt ?? typeConfig.systemPrompt;
2607
- const baseStopWhen = typeConfig.stopWhen ?? defaultStopWhen ?? stepCountIs(15);
2608
- const effectiveStopWhen = budget ? [baseStopWhen, budget.stopWhen].flat() : baseStopWhen;
2609
- const commonOptions = {
2610
- model,
2611
- tools,
2612
- system: systemPrompt,
2613
- prompt,
2614
- stopWhen: effectiveStopWhen,
2615
- prepareStep: typeConfig.prepareStep
2616
- };
2617
- if (streamWriter) {
2618
- const startId = generateEventId();
2619
- streamWriter.write({
2620
- type: "data-subagent",
2621
- id: startId,
2622
- data: {
2623
- event: "start",
2647
+ const executeTask = async () => {
2648
+ try {
2649
+ const model = typeConfig.model || defaultModel;
2650
+ const tools = filterTools(allTools, customTools ?? typeConfig.tools, typeConfig.additionalTools);
2651
+ const systemPrompt = system_prompt ?? typeConfig.systemPrompt;
2652
+ const baseStopWhen = typeConfig.stopWhen ?? defaultStopWhen ?? stepCountIs(15);
2653
+ const effectiveStopWhen = budget ? [baseStopWhen, budget.stopWhen].flat() : baseStopWhen;
2654
+ const commonOptions = {
2655
+ model,
2656
+ tools,
2657
+ system: systemPrompt,
2658
+ prompt,
2659
+ stopWhen: effectiveStopWhen,
2660
+ prepareStep: typeConfig.prepareStep
2661
+ };
2662
+ if (streamWriter) {
2663
+ const startId = generateEventId();
2664
+ streamWriter.write({
2665
+ type: "data-subagent",
2666
+ id: startId,
2667
+ data: {
2668
+ event: "start",
2669
+ subagent: subagent_type,
2670
+ description
2671
+ }
2672
+ });
2673
+ const result3 = streamText({
2674
+ ...commonOptions,
2675
+ onStepFinish: async (step) => {
2676
+ budget?.onStepFinish(step);
2677
+ if (step.toolCalls?.length) {
2678
+ for (const tc of step.toolCalls) {
2679
+ const eventId = generateEventId();
2680
+ streamWriter.write({
2681
+ type: "data-subagent",
2682
+ id: eventId,
2683
+ data: {
2684
+ event: "tool-call",
2685
+ subagent: subagent_type,
2686
+ description,
2687
+ toolName: tc.toolName,
2688
+ args: tc.input
2689
+ }
2690
+ });
2691
+ }
2692
+ }
2693
+ await typeConfig.onStepFinish?.(step);
2694
+ await defaultOnStepFinish?.({
2695
+ subagentType: subagent_type,
2696
+ description,
2697
+ step
2698
+ });
2699
+ }
2700
+ });
2701
+ const text = await result3.text;
2702
+ const usage2 = await result3.usage;
2703
+ const response = await result3.response;
2704
+ streamWriter.write({
2705
+ type: "data-subagent",
2706
+ id: generateEventId(),
2707
+ data: {
2708
+ event: "done",
2709
+ subagent: subagent_type,
2710
+ description
2711
+ }
2712
+ });
2713
+ streamWriter.write({
2714
+ type: "data-subagent",
2715
+ id: generateEventId(),
2716
+ data: {
2717
+ event: "complete",
2718
+ subagent: subagent_type,
2719
+ description,
2720
+ messages: response.messages
2721
+ }
2722
+ });
2723
+ const durationMs2 = Math.round(performance.now() - startTime);
2724
+ return {
2725
+ result: text,
2726
+ usage: usage2.inputTokens !== undefined && usage2.outputTokens !== undefined ? {
2727
+ input_tokens: usage2.inputTokens,
2728
+ output_tokens: usage2.outputTokens
2729
+ } : undefined,
2730
+ duration_ms: durationMs2,
2624
2731
  subagent: subagent_type,
2625
2732
  description
2626
- }
2627
- });
2628
- const result2 = streamText({
2733
+ };
2734
+ }
2735
+ const result2 = await generateText2({
2629
2736
  ...commonOptions,
2630
2737
  onStepFinish: async (step) => {
2631
2738
  budget?.onStepFinish(step);
2632
- if (step.toolCalls?.length) {
2633
- for (const tc of step.toolCalls) {
2634
- const eventId = generateEventId();
2635
- streamWriter.write({
2636
- type: "data-subagent",
2637
- id: eventId,
2638
- data: {
2639
- event: "tool-call",
2640
- subagent: subagent_type,
2641
- description,
2642
- toolName: tc.toolName,
2643
- args: tc.input
2644
- }
2645
- });
2646
- }
2647
- }
2648
2739
  await typeConfig.onStepFinish?.(step);
2649
2740
  await defaultOnStepFinish?.({
2650
2741
  subagentType: subagent_type,
@@ -2653,98 +2744,46 @@ function createTaskTool(config) {
2653
2744
  });
2654
2745
  }
2655
2746
  });
2656
- const text = await result2.text;
2657
- const usage2 = await result2.usage;
2658
- const response = await result2.response;
2659
- streamWriter.write({
2660
- type: "data-subagent",
2661
- id: generateEventId(),
2662
- data: {
2663
- event: "done",
2664
- subagent: subagent_type,
2665
- description
2666
- }
2667
- });
2668
- streamWriter.write({
2669
- type: "data-subagent",
2670
- id: generateEventId(),
2671
- data: {
2672
- event: "complete",
2673
- subagent: subagent_type,
2674
- description,
2675
- messages: response.messages
2676
- }
2677
- });
2678
- const durationMs2 = Math.round(performance.now() - startTime);
2679
- if (debugId) {
2680
- popParent();
2681
- debugEnd(debugId, "task", {
2682
- summary: {
2683
- tokens: {
2684
- input: usage2.inputTokens,
2685
- output: usage2.outputTokens
2686
- },
2687
- steps: response.messages?.length
2688
- },
2689
- duration_ms: durationMs2
2690
- });
2691
- }
2747
+ const durationMs = Math.round(performance.now() - startTime);
2748
+ const usage = result2.usage.inputTokens !== undefined && result2.usage.outputTokens !== undefined ? {
2749
+ input_tokens: result2.usage.inputTokens,
2750
+ output_tokens: result2.usage.outputTokens
2751
+ } : undefined;
2692
2752
  return {
2693
- result: text,
2694
- usage: usage2.inputTokens !== undefined && usage2.outputTokens !== undefined ? {
2695
- input_tokens: usage2.inputTokens,
2696
- output_tokens: usage2.outputTokens
2697
- } : undefined,
2698
- duration_ms: durationMs2,
2753
+ result: result2.text,
2754
+ usage,
2755
+ duration_ms: durationMs,
2699
2756
  subagent: subagent_type,
2700
2757
  description
2701
2758
  };
2759
+ } catch (error) {
2760
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
2761
+ const durationMs = Math.round(performance.now() - startTime);
2762
+ return {
2763
+ error: errorMessage,
2764
+ subagent: subagent_type,
2765
+ description,
2766
+ duration_ms: durationMs
2767
+ };
2702
2768
  }
2703
- const result = await generateText2({
2704
- ...commonOptions,
2705
- onStepFinish: async (step) => {
2706
- budget?.onStepFinish(step);
2707
- await typeConfig.onStepFinish?.(step);
2708
- await defaultOnStepFinish?.({
2709
- subagentType: subagent_type,
2710
- description,
2711
- step
2712
- });
2713
- }
2714
- });
2715
- const durationMs = Math.round(performance.now() - startTime);
2716
- const usage = result.usage.inputTokens !== undefined && result.usage.outputTokens !== undefined ? {
2717
- input_tokens: result.usage.inputTokens,
2718
- output_tokens: result.usage.outputTokens
2719
- } : undefined;
2720
- if (debugId) {
2721
- popParent();
2769
+ };
2770
+ const result = await runWithDebugParent(debugId, executeTask);
2771
+ if (debugId) {
2772
+ if ("error" in result) {
2773
+ debugError(debugId, "task", result.error);
2774
+ } else {
2722
2775
  debugEnd(debugId, "task", {
2723
2776
  summary: {
2724
- tokens: {
2725
- input: result.usage.inputTokens,
2726
- output: result.usage.outputTokens
2727
- },
2728
- steps: result.steps?.length
2777
+ tokens: result.usage ? {
2778
+ input: result.usage.input_tokens,
2779
+ output: result.usage.output_tokens
2780
+ } : undefined
2729
2781
  },
2730
- duration_ms: durationMs
2782
+ duration_ms: result.duration_ms ?? Math.round(performance.now() - startTime)
2731
2783
  });
2732
2784
  }
2733
- return {
2734
- result: result.text,
2735
- usage,
2736
- duration_ms: durationMs,
2737
- subagent: subagent_type,
2738
- description
2739
- };
2740
- } catch (error) {
2741
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
2742
- if (debugId) {
2743
- popParent();
2744
- debugError(debugId, "task", errorMessage);
2745
- }
2746
- return { error: errorMessage };
2747
2785
  }
2786
+ return result;
2748
2787
  }
2749
2788
  });
2750
2789
  }
@@ -2939,31 +2978,39 @@ async function createAgentTools(sandbox, config) {
2939
2978
  }
2940
2979
  }
2941
2980
  }
2981
+ let openRouterModels;
2982
+ const shouldFetchOpenRouter = config?.modelRegistry?.provider === "openRouter" || config?.budget?.pricingProvider === "openRouter";
2983
+ if (shouldFetchOpenRouter) {
2984
+ const apiKey = config?.modelRegistry?.apiKey ?? config?.budget?.apiKey;
2985
+ try {
2986
+ openRouterModels = await fetchOpenRouterModels(apiKey);
2987
+ } catch (err) {
2988
+ if (config?.budget && !config.budget.modelPricing) {
2989
+ throw new Error(`[bashkit] Failed to fetch OpenRouter pricing and no modelPricing overrides provided. ` + `Either provide modelPricing in your budget config or ensure network access to OpenRouter. ` + `Original error: ${err instanceof Error ? err.message : String(err)}`);
2990
+ }
2991
+ }
2992
+ }
2993
+ let openRouterPricing;
2994
+ if (openRouterModels) {
2995
+ openRouterPricing = new Map([...openRouterModels].map(([k, v]) => [k, v.pricing]));
2996
+ }
2942
2997
  let budget;
2943
2998
  if (config?.budget) {
2944
- const { pricingProvider, apiKey, modelPricing, maxUsd } = config.budget;
2945
- if (!pricingProvider && !modelPricing) {
2946
- throw new Error("[bashkit] Budget requires either pricingProvider or modelPricing (or both).");
2947
- }
2948
- let openRouterPricing;
2949
- if (pricingProvider === "openRouter") {
2950
- try {
2951
- openRouterPricing = await fetchOpenRouterPricing(apiKey);
2952
- } catch (err) {
2953
- if (!modelPricing) {
2954
- throw new Error(`[bashkit] Failed to fetch OpenRouter pricing and no modelPricing overrides provided. ` + `Either provide modelPricing in your budget config or ensure network access to OpenRouter. ` + `Original error: ${err instanceof Error ? err.message : String(err)}`);
2955
- }
2956
- }
2999
+ const { modelPricing, maxUsd } = config.budget;
3000
+ if (!openRouterPricing && !modelPricing) {
3001
+ throw new Error("[bashkit] Budget requires either modelRegistry, pricingProvider, or modelPricing.");
2957
3002
  }
2958
3003
  budget = createBudgetTracker(maxUsd, {
2959
3004
  modelPricing,
2960
3005
  openRouterPricing
2961
3006
  });
2962
3007
  }
2963
- return { tools, planModeState, budget };
3008
+ return { tools, planModeState, budget, openRouterModels };
2964
3009
  }
2965
3010
  // src/utils/compact-conversation.ts
2966
- import { generateText as generateText3 } from "ai";
3011
+ import {
3012
+ generateText as generateText3
3013
+ } from "ai";
2967
3014
 
2968
3015
  // src/utils/prune-messages.ts
2969
3016
  var DEFAULT_CONFIG2 = {
@@ -2984,10 +3031,10 @@ function estimateMessageTokens(message) {
2984
3031
  tokens += estimateTokens(part);
2985
3032
  } else if ("text" in part && typeof part.text === "string") {
2986
3033
  tokens += estimateTokens(part.text);
2987
- } else if ("result" in part) {
2988
- tokens += estimateTokens(JSON.stringify(part.result));
2989
- } else if ("args" in part) {
2990
- tokens += estimateTokens(JSON.stringify(part.args));
3034
+ } else if (isToolResultPart(part)) {
3035
+ tokens += estimateTokens(JSON.stringify(part.output));
3036
+ } else if (isToolCallPart(part)) {
3037
+ tokens += estimateTokens(JSON.stringify(part.input));
2991
3038
  } else {
2992
3039
  tokens += estimateTokens(JSON.stringify(part));
2993
3040
  }
@@ -3029,10 +3076,10 @@ function pruneMessageContent(message) {
3029
3076
  return message;
3030
3077
  }
3031
3078
  const prunedContent = message.content.map((part) => {
3032
- if (typeof part === "object" && "toolName" in part && "args" in part) {
3079
+ if (isToolCallPart(part)) {
3033
3080
  return {
3034
3081
  ...part,
3035
- args: { _pruned: true, toolName: part.toolName }
3082
+ input: { _pruned: true, toolName: part.toolName }
3036
3083
  };
3037
3084
  }
3038
3085
  return part;
@@ -3047,10 +3094,10 @@ function pruneToolMessage(message) {
3047
3094
  return message;
3048
3095
  }
3049
3096
  const prunedContent = message.content.map((part) => {
3050
- if (typeof part === "object" && "result" in part) {
3097
+ if (isToolResultPart(part)) {
3051
3098
  return {
3052
3099
  ...part,
3053
- result: { _pruned: true }
3100
+ output: { type: "text", value: "pruned" }
3054
3101
  };
3055
3102
  }
3056
3103
  return part;
@@ -3099,7 +3146,53 @@ function pruneMessagesByTokens(messages, config) {
3099
3146
  return prunedMessages;
3100
3147
  }
3101
3148
 
3149
+ // src/utils/context-status.ts
3150
+ var DEFAULT_CONFIG3 = {
3151
+ elevatedThreshold: 0.5,
3152
+ highThreshold: 0.7,
3153
+ criticalThreshold: 0.85
3154
+ };
3155
+ function defaultHighGuidance(metrics) {
3156
+ const used = Math.round(metrics.usagePercent * 100);
3157
+ const remaining = Math.round((1 - metrics.usagePercent) * 100);
3158
+ return `Context usage: ${used}%. You still have ${remaining}% remaining—no need to rush. Continue working thoroughly.`;
3159
+ }
3160
+ function defaultCriticalGuidance(metrics) {
3161
+ const used = Math.round(metrics.usagePercent * 100);
3162
+ return `Context usage: ${used}%. Consider wrapping up the current task or summarizing progress before continuing.`;
3163
+ }
3164
+ function getContextStatus(messages, maxTokens, config) {
3165
+ const { elevatedThreshold, highThreshold, criticalThreshold } = {
3166
+ ...DEFAULT_CONFIG3,
3167
+ ...config
3168
+ };
3169
+ const usedTokens = config?.knownTokenCount ?? estimateMessagesTokens(messages);
3170
+ const usagePercent = usedTokens / maxTokens;
3171
+ const baseStatus = { usedTokens, maxTokens, usagePercent };
3172
+ if (usagePercent < elevatedThreshold) {
3173
+ return { ...baseStatus, status: "comfortable" };
3174
+ }
3175
+ if (usagePercent < highThreshold) {
3176
+ return { ...baseStatus, status: "elevated" };
3177
+ }
3178
+ if (usagePercent < criticalThreshold) {
3179
+ const guidance2 = typeof config?.highGuidance === "function" ? config.highGuidance(baseStatus) : config?.highGuidance ?? defaultHighGuidance(baseStatus);
3180
+ return { ...baseStatus, status: "high", guidance: guidance2 };
3181
+ }
3182
+ const guidance = typeof config?.criticalGuidance === "function" ? config.criticalGuidance(baseStatus) : config?.criticalGuidance ?? defaultCriticalGuidance(baseStatus);
3183
+ return { ...baseStatus, status: "critical", guidance };
3184
+ }
3185
+ function contextNeedsAttention(status) {
3186
+ return status.status === "high" || status.status === "critical";
3187
+ }
3188
+ function contextNeedsCompaction(status) {
3189
+ return status.status === "critical";
3190
+ }
3191
+
3102
3192
  // src/utils/compact-conversation.ts
3193
+ class CompactionError extends Error {
3194
+ name = "CompactionError";
3195
+ }
3103
3196
  async function compactConversation(messages, config, state2 = { conversationSummary: "" }) {
3104
3197
  const currentTokens = estimateMessagesTokens(messages);
3105
3198
  const threshold = config.compactionThreshold ?? 0.85;
@@ -3108,12 +3201,14 @@ async function compactConversation(messages, config, state2 = { conversationSumm
3108
3201
  return { messages, state: state2, didCompact: false };
3109
3202
  }
3110
3203
  const protectCount = config.protectRecentMessages ?? 10;
3111
- const recentMessages = messages.slice(-protectCount);
3112
- const oldMessages = messages.slice(0, -protectCount);
3204
+ const splitAt = findSafeSplitIndex(messages, protectCount);
3205
+ const oldMessages = messages.slice(0, splitAt);
3206
+ const recentMessages = messages.slice(splitAt);
3113
3207
  if (oldMessages.length === 0) {
3114
3208
  return { messages, state: state2, didCompact: false };
3115
3209
  }
3116
- const newSummary = await summarizeMessages(oldMessages, config.summarizerModel, config.taskContext, state2.conversationSummary);
3210
+ const fileOps = extractFileOps(oldMessages);
3211
+ const newSummary = await summarizeMessages(oldMessages, config.summarizerModel, config.taskContext, state2.conversationSummary, fileOps, config.summaryInstructions);
3117
3212
  const compactedMessages = [
3118
3213
  {
3119
3214
  role: "user",
@@ -3151,6 +3246,8 @@ Create a structured summary of the conversation below. This summary will replace
3151
3246
  {{PREVIOUS_SUMMARY}}
3152
3247
  </previous-summary>
3153
3248
 
3249
+ {{FILE_OPERATIONS}}
3250
+
3154
3251
  <conversation-to-summarize>
3155
3252
  {{CONVERSATION}}
3156
3253
  </conversation-to-summarize>
@@ -3197,9 +3294,44 @@ Brief description of what the user asked for and the current goal.
3197
3294
  - Maintain the user's original terminology and naming.
3198
3295
  - Do not editorialize or add suggestions - just capture what happened.
3199
3296
  - Omit sections that have no relevant information.
3200
- </instructions>`;
3201
- async function summarizeMessages(messages, model, taskContext, previousSummary) {
3202
- const prompt = SUMMARIZATION_PROMPT.replace("{{TASK_CONTEXT}}", taskContext || "Not specified").replace("{{PREVIOUS_SUMMARY}}", previousSummary || "None - this is the first compaction").replace("{{CONVERSATION}}", formatMessagesForSummary(messages));
3297
+ {{SUMMARY_INSTRUCTIONS}}</instructions>`;
3298
+ async function summarizeMessages(messages, model, taskContext, previousSummary, fileOps, summaryInstructions) {
3299
+ let fileOpsBlock = "";
3300
+ if (fileOps) {
3301
+ const MAX_FILES = 50;
3302
+ const sanitize = (p) => p.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3303
+ const readFiles = [...fileOps.read].sort().map(sanitize);
3304
+ const modifiedFiles = [...fileOps.modified].sort().map(sanitize);
3305
+ const sections = [];
3306
+ if (readFiles.length > 0) {
3307
+ const listed = readFiles.slice(0, MAX_FILES);
3308
+ sections.push(`Read: ${listed.join(", ")}`);
3309
+ if (readFiles.length > MAX_FILES) {
3310
+ sections.push(`... and ${readFiles.length - MAX_FILES} more read files`);
3311
+ }
3312
+ }
3313
+ if (modifiedFiles.length > 0) {
3314
+ const listed = modifiedFiles.slice(0, MAX_FILES);
3315
+ sections.push(`Modified: ${listed.join(", ")}`);
3316
+ if (modifiedFiles.length > MAX_FILES) {
3317
+ sections.push(`... and ${modifiedFiles.length - MAX_FILES} more modified files`);
3318
+ }
3319
+ }
3320
+ if (sections.length > 0) {
3321
+ fileOpsBlock = `<file-operations>
3322
+ ${sections.join(`
3323
+ `)}
3324
+ </file-operations>`;
3325
+ }
3326
+ }
3327
+ const templateVars = {
3328
+ TASK_CONTEXT: taskContext || "Not specified",
3329
+ PREVIOUS_SUMMARY: previousSummary || "None - this is the first compaction",
3330
+ CONVERSATION: formatMessagesForSummary(messages),
3331
+ FILE_OPERATIONS: fileOpsBlock,
3332
+ SUMMARY_INSTRUCTIONS: summaryInstructions ? `- ${summaryInstructions}` : ""
3333
+ };
3334
+ const prompt = SUMMARIZATION_PROMPT.replace(/\{\{(\w+)\}\}/g, (_, key) => templateVars[key] ?? "");
3203
3335
  const result = await generateText3({
3204
3336
  model,
3205
3337
  messages: [
@@ -3211,6 +3343,12 @@ async function summarizeMessages(messages, model, taskContext, previousSummary)
3211
3343
  });
3212
3344
  return result.text;
3213
3345
  }
3346
+ var MAX_PART_LENGTH = 500;
3347
+ function truncate(str) {
3348
+ if (str.length <= MAX_PART_LENGTH)
3349
+ return str;
3350
+ return `${str.slice(0, MAX_PART_LENGTH)}... [truncated]`;
3351
+ }
3214
3352
  function formatMessagesForSummary(messages) {
3215
3353
  return messages.map((msg, index) => {
3216
3354
  const role = msg.role.toUpperCase();
@@ -3227,16 +3365,17 @@ ${msg.content}
3227
3365
  if ("text" in part && typeof part.text === "string") {
3228
3366
  return part.text;
3229
3367
  }
3230
- if ("toolName" in part && "args" in part) {
3368
+ if (isToolCallPart(part)) {
3369
+ const inputStr = truncate(JSON.stringify(part.input));
3231
3370
  return `[Tool Call: ${part.toolName}]
3232
- Args: ${JSON.stringify(part.args, null, 2)}`;
3371
+ Input: ${inputStr}`;
3233
3372
  }
3234
- if ("result" in part) {
3235
- const resultStr = typeof part.result === "string" ? part.result : JSON.stringify(part.result, null, 2);
3373
+ if (isToolResultPart(part)) {
3374
+ const outputStr = typeof part.output === "string" ? part.output : JSON.stringify(part.output);
3236
3375
  return `[Tool Result]
3237
- ${resultStr}`;
3376
+ ${truncate(outputStr)}`;
3238
3377
  }
3239
- return JSON.stringify(part, null, 2);
3378
+ return truncate(JSON.stringify(part));
3240
3379
  }).join(`
3241
3380
 
3242
3381
  `);
@@ -3245,12 +3384,117 @@ ${parts}
3245
3384
  </message>`;
3246
3385
  }
3247
3386
  return `<message index="${index}" role="${role}">
3248
- ${JSON.stringify(msg.content, null, 2)}
3387
+ ${truncate(JSON.stringify(msg.content))}
3249
3388
  </message>`;
3250
3389
  }).join(`
3251
3390
 
3252
3391
  `);
3253
3392
  }
3393
+ function hasToolCalls(message) {
3394
+ if (message.role !== "assistant" || !Array.isArray(message.content)) {
3395
+ return false;
3396
+ }
3397
+ return message.content.some(isToolCallPart);
3398
+ }
3399
+ function findSafeSplitIndex(messages, protectCount) {
3400
+ const naiveSplit = Math.max(0, messages.length - protectCount);
3401
+ let splitAt = naiveSplit;
3402
+ while (splitAt > 0) {
3403
+ const msg = messages[splitAt];
3404
+ if (msg.role === "tool") {
3405
+ splitAt--;
3406
+ continue;
3407
+ }
3408
+ const prev = messages[splitAt - 1];
3409
+ if (prev?.role === "assistant" && hasToolCalls(prev)) {
3410
+ splitAt--;
3411
+ continue;
3412
+ }
3413
+ break;
3414
+ }
3415
+ if (splitAt === 0 && naiveSplit > 0) {
3416
+ splitAt = naiveSplit;
3417
+ while (splitAt < messages.length) {
3418
+ const msg = messages[splitAt];
3419
+ if (msg.role !== "tool") {
3420
+ const prev = messages[splitAt - 1];
3421
+ if (!prev || prev.role !== "assistant" || !hasToolCalls(prev)) {
3422
+ break;
3423
+ }
3424
+ }
3425
+ splitAt++;
3426
+ }
3427
+ if (splitAt >= messages.length) {
3428
+ splitAt = naiveSplit;
3429
+ }
3430
+ }
3431
+ return splitAt;
3432
+ }
3433
+ function extractFileOps(messages) {
3434
+ const ops = {
3435
+ read: new Set,
3436
+ modified: new Set
3437
+ };
3438
+ for (const msg of messages) {
3439
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
3440
+ continue;
3441
+ for (const part of msg.content) {
3442
+ if (!isToolCallPart(part))
3443
+ continue;
3444
+ const toolName = String(part.toolName).toLowerCase();
3445
+ const rawInput = part.input;
3446
+ if (typeof rawInput !== "object" || rawInput === null)
3447
+ continue;
3448
+ const input = rawInput;
3449
+ switch (toolName) {
3450
+ case "read": {
3451
+ const filePath = input.file_path;
3452
+ if (typeof filePath === "string")
3453
+ ops.read.add(filePath);
3454
+ break;
3455
+ }
3456
+ case "write":
3457
+ case "edit": {
3458
+ const filePath = input.file_path;
3459
+ if (typeof filePath === "string")
3460
+ ops.modified.add(filePath);
3461
+ break;
3462
+ }
3463
+ }
3464
+ }
3465
+ }
3466
+ return ops;
3467
+ }
3468
+ function createAutoCompaction(config) {
3469
+ const state2 = { conversationSummary: "" };
3470
+ const threshold = config.compactionThreshold ?? 0.85;
3471
+ const prepareStep = async (args) => {
3472
+ const status = getContextStatus(args.messages, config.maxTokens, {
3473
+ criticalThreshold: threshold
3474
+ });
3475
+ if (status.status !== "critical") {
3476
+ return {};
3477
+ }
3478
+ let lastError;
3479
+ for (let attempt = 0;attempt < 2; attempt++) {
3480
+ try {
3481
+ const result = await compactConversation(args.messages, config, state2);
3482
+ if (result.didCompact) {
3483
+ state2.conversationSummary = result.state.conversationSummary;
3484
+ return { messages: result.messages };
3485
+ }
3486
+ return {};
3487
+ } catch (err) {
3488
+ lastError = err;
3489
+ if (attempt < 1) {
3490
+ await new Promise((resolve) => setTimeout(resolve, 1000));
3491
+ }
3492
+ }
3493
+ }
3494
+ throw new CompactionError("Conversation compaction failed after 2 attempts", { cause: lastError });
3495
+ };
3496
+ return { prepareStep, getState: () => ({ ...state2 }) };
3497
+ }
3254
3498
  var MODEL_CONTEXT_LIMITS = {
3255
3499
  "claude-opus-4-5": 200000,
3256
3500
  "claude-sonnet-4-5": 200000,
@@ -3270,47 +3514,16 @@ function createCompactConfig(modelId, summarizerModel, overrides) {
3270
3514
  ...overrides
3271
3515
  };
3272
3516
  }
3273
- // src/utils/context-status.ts
3274
- var DEFAULT_CONFIG3 = {
3275
- elevatedThreshold: 0.5,
3276
- highThreshold: 0.7,
3277
- criticalThreshold: 0.85
3278
- };
3279
- function defaultHighGuidance(metrics) {
3280
- const used = Math.round(metrics.usagePercent * 100);
3281
- const remaining = Math.round((1 - metrics.usagePercent) * 100);
3282
- return `Context usage: ${used}%. You still have ${remaining}% remaining—no need to rush. Continue working thoroughly.`;
3283
- }
3284
- function defaultCriticalGuidance(metrics) {
3285
- const used = Math.round(metrics.usagePercent * 100);
3286
- return `Context usage: ${used}%. Consider wrapping up the current task or summarizing progress before continuing.`;
3287
- }
3288
- function getContextStatus(messages, maxTokens, config) {
3289
- const { elevatedThreshold, highThreshold, criticalThreshold } = {
3290
- ...DEFAULT_CONFIG3,
3291
- ...config
3292
- };
3293
- const usedTokens = estimateMessagesTokens(messages);
3294
- const usagePercent = usedTokens / maxTokens;
3295
- const baseStatus = { usedTokens, maxTokens, usagePercent };
3296
- if (usagePercent < elevatedThreshold) {
3297
- return { ...baseStatus, status: "comfortable" };
3298
- }
3299
- if (usagePercent < highThreshold) {
3300
- return { ...baseStatus, status: "elevated" };
3517
+ function createCompactConfigFromModels(modelId, summarizerModel, modelsMap, overrides) {
3518
+ const contextLength = getModelContextLength(modelId, modelsMap);
3519
+ if (contextLength === undefined) {
3520
+ throw new Error(`[bashkit] No context length found for model "${modelId}" in OpenRouter data. ` + `Provide maxTokens manually via overrides or use createCompactConfig() with a known model.`);
3301
3521
  }
3302
- if (usagePercent < criticalThreshold) {
3303
- const guidance2 = typeof config?.highGuidance === "function" ? config.highGuidance(baseStatus) : config?.highGuidance ?? defaultHighGuidance(baseStatus);
3304
- return { ...baseStatus, status: "high", guidance: guidance2 };
3305
- }
3306
- const guidance = typeof config?.criticalGuidance === "function" ? config.criticalGuidance(baseStatus) : config?.criticalGuidance ?? defaultCriticalGuidance(baseStatus);
3307
- return { ...baseStatus, status: "critical", guidance };
3308
- }
3309
- function contextNeedsAttention(status) {
3310
- return status.status === "high" || status.status === "critical";
3311
- }
3312
- function contextNeedsCompaction(status) {
3313
- return status.status === "critical";
3522
+ return {
3523
+ maxTokens: contextLength,
3524
+ summarizerModel,
3525
+ ...overrides
3526
+ };
3314
3527
  }
3315
3528
  // src/skills/discovery.ts
3316
3529
  import { readdir as readdir2, readFile as readFile2, stat } from "node:fs/promises";
@@ -3654,10 +3867,12 @@ export {
3654
3867
  loadSkillBundles,
3655
3868
  loadSkillBundle,
3656
3869
  isDebugEnabled,
3870
+ getModelContextLength,
3657
3871
  getDebugLogs,
3658
3872
  getContextStatus,
3659
3873
  fetchSkills,
3660
3874
  fetchSkill,
3875
+ fetchOpenRouterModels,
3661
3876
  estimateTokens,
3662
3877
  estimateMessagesTokens,
3663
3878
  estimateMessageTokens,
@@ -3679,9 +3894,11 @@ export {
3679
3894
  createEnterPlanModeTool,
3680
3895
  createEditTool,
3681
3896
  createE2BSandbox,
3897
+ createCompactConfigFromModels,
3682
3898
  createCompactConfig,
3683
3899
  createBudgetTracker,
3684
3900
  createBashTool,
3901
+ createAutoCompaction,
3685
3902
  createAskUserTool,
3686
3903
  createAgentTools,
3687
3904
  contextNeedsCompaction,
@@ -3693,5 +3910,6 @@ export {
3693
3910
  anthropicPromptCacheMiddleware,
3694
3911
  MODEL_CONTEXT_LIMITS,
3695
3912
  LRUCacheStore,
3696
- DEFAULT_CONFIG
3913
+ DEFAULT_CONFIG,
3914
+ CompactionError
3697
3915
  };