bashkit 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -92,6 +92,21 @@ This is useful for:
92
92
  - Persisting sandbox state between server restarts
93
93
  - Reducing sandbox creation overhead
94
94
 
95
+ ## Internal Architecture
96
+
97
+ For developers working on bashkit internals, each source folder has its own `AGENTS.md`:
98
+
99
+ - `src/sandbox/AGENTS.md` -- Execution environment abstractions
100
+ - `src/tools/AGENTS.md` -- Tool implementations
101
+ - `src/cache/AGENTS.md` -- Tool result caching
102
+ - `src/middleware/AGENTS.md` -- AI SDK middleware
103
+ - `src/utils/AGENTS.md` -- Utility functions
104
+ - `src/skills/AGENTS.md` -- Agent Skills support
105
+ - `src/setup/AGENTS.md` -- Environment setup
106
+ - `src/cli/AGENTS.md` -- CLI initialization
107
+
108
+ See also `CLAUDE.md` for development workflow and conventions.
109
+
95
110
  ## Available Tools
96
111
 
97
112
  ### Default Tools (always included)
package/dist/cli/init.js CHANGED
@@ -54,7 +54,7 @@ export const config: AgentConfig = {${webTools ? `
54
54
  ` : ""}};
55
55
 
56
56
  // Create tools
57
- export const { tools } = createAgentTools(sandbox${webTools ? ", config" : ""});
57
+ export const { tools } = await createAgentTools(sandbox${webTools ? ", config" : ""});
58
58
  `;
59
59
  const configPath = join(process.cwd(), "bashkit.config.ts");
60
60
  if (existsSync(configPath)) {
package/dist/index.d.ts CHANGED
@@ -5,12 +5,12 @@ export { createE2BSandbox, createLocalSandbox, createVercelSandbox, ensureSandbo
5
5
  export type { ExecOptions, ExecResult, Sandbox } from "./sandbox/interface";
6
6
  export type { AgentToolsResult, AskUserError, AskUserOutput, AskUserResponseHandler, QuestionOption, StructuredQuestion, BashError, BashOutput, EditError, EditOutput, EnterPlanModeError, EnterPlanModeOutput, ExitPlanModeError, ExitPlanModeOutput, PlanModeState, GlobError, GlobOutput, GrepContentOutput, GrepCountOutput, GrepError, GrepFilesOutput, GrepMatch, GrepOutput, ReadDirectoryOutput, ReadError, ReadOutput, ReadTextOutput, SkillError, SkillOutput, SkillToolConfig, SubagentEventData, SubagentStepEvent, SubagentTypeConfig, TaskError, TaskOutput, TaskToolConfig, TodoItem, TodoState, TodoWriteError, TodoWriteOutput, WebFetchError, WebFetchOutput, WebSearchError, WebSearchOutput, WebSearchResult, WriteError, WriteOutput, } from "./tools";
7
7
  export { createAgentTools, createAskUserTool, createBashTool, createEditTool, createEnterPlanModeTool, createExitPlanModeTool, createGlobTool, createGrepTool, createReadTool, createSkillTool, createTaskTool, createTodoWriteTool, createWebFetchTool, createWebSearchTool, createWriteTool, } from "./tools";
8
- export type { AgentConfig, AskUserConfig, CacheConfig, SkillConfig, ToolConfig, WebFetchConfig, WebSearchConfig, } from "./types";
8
+ export type { AgentConfig, AskUserConfig, BudgetConfig, CacheConfig, PricingProvider, SkillConfig, ToolConfig, WebFetchConfig, WebSearchConfig, } from "./types";
9
9
  export { DEFAULT_CONFIG } from "./types";
10
10
  export type { CachedTool, CacheEntry, CacheOptions, CacheStats, CacheStore, RedisCacheStoreOptions, RedisClient, } from "./cache";
11
11
  export { cached, createRedisCacheStore, LRUCacheStore } from "./cache";
12
- export type { CompactConversationConfig, CompactConversationResult, CompactConversationState, ContextMetrics, ContextStatus, ContextStatusConfig, ContextStatusLevel, DebugEvent, ModelContextLimit, PruneMessagesConfig, } from "./utils";
13
- export { clearDebugLogs, compactConversation, contextNeedsAttention, contextNeedsCompaction, createCompactConfig, estimateMessagesTokens, estimateMessageTokens, estimateTokens, getContextStatus, getDebugLogs, isDebugEnabled, MODEL_CONTEXT_LIMITS, pruneMessagesByTokens, reinitDebugMode, } from "./utils";
12
+ export type { BudgetStatus, BudgetTracker, ModelPricing, CompactConversationConfig, CompactConversationResult, CompactConversationState, ContextMetrics, ContextStatus, ContextStatusConfig, ContextStatusLevel, DebugEvent, ModelContextLimit, PruneMessagesConfig, } from "./utils";
13
+ export { createBudgetTracker, clearDebugLogs, compactConversation, contextNeedsAttention, contextNeedsCompaction, createCompactConfig, estimateMessagesTokens, estimateMessageTokens, estimateTokens, getContextStatus, getDebugLogs, isDebugEnabled, MODEL_CONTEXT_LIMITS, pruneMessagesByTokens, reinitDebugMode, } from "./utils";
14
14
  export type { DiscoverSkillsOptions, SkillBundle, SkillMetadata, } from "./skills";
15
15
  export { discoverSkills, fetchSkill, fetchSkills, loadSkillBundle, loadSkillBundles, parseSkillMetadata, skillsToXml, } from "./skills";
16
16
  export type { AgentEnvironmentConfig, SetupResult, SkillContent, } from "./setup";
package/dist/index.js CHANGED
@@ -612,6 +612,242 @@ var DEFAULT_CONFIG = {
612
612
  }
613
613
  };
614
614
 
615
+ // src/utils/budget-tracking.ts
616
+ var openRouterCache = null;
617
+ var openRouterCacheTimestamp = 0;
618
+ var openRouterFetchPromise = null;
619
+ var OPENROUTER_FETCH_TIMEOUT_MS = 1e4;
620
+ 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;
624
+ }
625
+ if (openRouterFetchPromise)
626
+ return openRouterFetchPromise;
627
+ openRouterFetchPromise = (async () => {
628
+ const controller = new AbortController;
629
+ const timeoutId = setTimeout(() => controller.abort(), OPENROUTER_FETCH_TIMEOUT_MS);
630
+ try {
631
+ let response;
632
+ try {
633
+ const headers = {};
634
+ if (apiKey) {
635
+ headers["Authorization"] = `Bearer ${apiKey}`;
636
+ }
637
+ response = await fetch("https://openrouter.ai/api/v1/models", {
638
+ signal: controller.signal,
639
+ headers
640
+ });
641
+ } catch (err) {
642
+ if (err instanceof DOMException && err.name === "AbortError") {
643
+ throw new Error(`[bashkit] OpenRouter pricing fetch timed out after ${OPENROUTER_FETCH_TIMEOUT_MS / 1000}s. ` + `This usually means OpenRouter is unreachable from your network. ` + `You can bypass this by providing modelPricing overrides in your config.`);
644
+ }
645
+ throw new Error(`[bashkit] OpenRouter pricing fetch failed (network error). ` + `Ensure you have internet access or provide modelPricing overrides in your config. ` + `Original error: ${err instanceof Error ? err.message : String(err)}`);
646
+ }
647
+ if (!response.ok) {
648
+ throw new Error(`[bashkit] OpenRouter pricing fetch failed: HTTP ${response.status} ${response.statusText}. ` + `You can bypass this by providing modelPricing overrides in your config.`);
649
+ }
650
+ const json = await response.json();
651
+ const models = json.data;
652
+ if (!Array.isArray(models)) {
653
+ throw new Error(`[bashkit] OpenRouter pricing response missing data array. ` + `The API may have changed. Please provide modelPricing overrides in your config.`);
654
+ }
655
+ const MAX_MODELS = 1e4;
656
+ const map = new Map;
657
+ for (const model of models.slice(0, MAX_MODELS)) {
658
+ if (!model.id || !model.pricing)
659
+ continue;
660
+ const prompt = parseFloat(model.pricing.prompt ?? "");
661
+ const completion = parseFloat(model.pricing.completion ?? "");
662
+ if (!Number.isFinite(prompt) || !Number.isFinite(completion))
663
+ continue;
664
+ if (prompt < 0 || completion < 0)
665
+ continue;
666
+ const pricing = {
667
+ inputPerToken: prompt,
668
+ outputPerToken: completion
669
+ };
670
+ const cacheRead = parseFloat(model.pricing.input_cache_read ?? "");
671
+ if (Number.isFinite(cacheRead) && cacheRead >= 0)
672
+ pricing.cacheReadPerToken = cacheRead;
673
+ const cacheWrite = parseFloat(model.pricing.input_cache_write ?? "");
674
+ if (Number.isFinite(cacheWrite) && cacheWrite >= 0)
675
+ pricing.cacheWritePerToken = cacheWrite;
676
+ map.set(model.id.toLowerCase(), pricing);
677
+ }
678
+ openRouterCache = map;
679
+ openRouterCacheTimestamp = Date.now();
680
+ return openRouterCache;
681
+ } finally {
682
+ clearTimeout(timeoutId);
683
+ openRouterFetchPromise = null;
684
+ }
685
+ })();
686
+ return openRouterFetchPromise;
687
+ }
688
+ function getModelMatchVariants(model) {
689
+ const lower = model.toLowerCase();
690
+ const kebab = lower.replace(/[^a-z0-9]+/g, "-");
691
+ const withoutProvider = lower.includes("/") ? lower.slice(lower.lastIndexOf("/") + 1) : lower;
692
+ const withoutProviderKebab = withoutProvider.replace(/[^a-z0-9]+/g, "-");
693
+ const seen = new Set;
694
+ const variants = [];
695
+ for (const v of [lower, kebab, withoutProvider, withoutProviderKebab]) {
696
+ if (!seen.has(v)) {
697
+ seen.add(v);
698
+ variants.push(v);
699
+ }
700
+ }
701
+ return variants;
702
+ }
703
+ function searchModelInCosts(model, costsMap) {
704
+ if (costsMap.size === 0)
705
+ return;
706
+ const modelVariants = getModelMatchVariants(model);
707
+ const costVariantsCache = new Map;
708
+ function getCostVariants(key) {
709
+ let variants = costVariantsCache.get(key);
710
+ if (!variants) {
711
+ variants = getModelMatchVariants(key);
712
+ costVariantsCache.set(key, variants);
713
+ }
714
+ return variants;
715
+ }
716
+ for (const variant of modelVariants) {
717
+ const pricing = costsMap.get(variant);
718
+ if (pricing)
719
+ return pricing;
720
+ }
721
+ let bestMatch;
722
+ let bestMatchLength = 0;
723
+ for (const [costKey, pricing] of costsMap) {
724
+ const costVariants = getCostVariants(costKey);
725
+ 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;
730
+ }
731
+ }
732
+ }
733
+ }
734
+ if (bestMatch)
735
+ return bestMatch;
736
+ let reverseMatch;
737
+ let reverseMatchLength = Infinity;
738
+ for (const [costKey, pricing] of costsMap) {
739
+ const costVariants = getCostVariants(costKey);
740
+ 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;
745
+ }
746
+ }
747
+ }
748
+ }
749
+ return reverseMatch;
750
+ }
751
+ function findPricingForModel(model, options) {
752
+ if (options?.overrides) {
753
+ const overrideMap = options.overrides instanceof Map ? options.overrides : new Map(Object.entries(options.overrides).map(([k, v]) => [
754
+ k.toLowerCase(),
755
+ v
756
+ ]));
757
+ const found = searchModelInCosts(model, overrideMap);
758
+ if (found)
759
+ return found;
760
+ }
761
+ if (options?.openRouterCache) {
762
+ const found = searchModelInCosts(model, options.openRouterCache);
763
+ if (found)
764
+ return found;
765
+ }
766
+ if (options?.warnedModels && !options.warnedModels.has(model.toLowerCase())) {
767
+ options.warnedModels.add(model.toLowerCase());
768
+ console.warn(`[bashkit] No pricing found for model "${model}". Cost will not be tracked for this step.`);
769
+ }
770
+ return;
771
+ }
772
+ function calculateStepCost(usage, pricing) {
773
+ let inputCost;
774
+ const cacheRead = usage.inputTokenDetails?.cacheReadTokens;
775
+ const cacheWrite = usage.inputTokenDetails?.cacheWriteTokens;
776
+ const noCache = usage.inputTokenDetails?.noCacheTokens;
777
+ if (cacheRead != null || cacheWrite != null || noCache != null) {
778
+ const noCacheCost = (noCache ?? 0) * pricing.inputPerToken;
779
+ const cacheReadCost = (cacheRead ?? 0) * (pricing.cacheReadPerToken ?? pricing.inputPerToken);
780
+ const cacheWriteCost = (cacheWrite ?? 0) * (pricing.cacheWritePerToken ?? pricing.inputPerToken);
781
+ inputCost = noCacheCost + cacheReadCost + cacheWriteCost;
782
+ } else {
783
+ inputCost = (usage.inputTokens ?? 0) * pricing.inputPerToken;
784
+ }
785
+ const outputCost = (usage.outputTokens ?? 0) * pricing.outputPerToken;
786
+ return inputCost + outputCost;
787
+ }
788
+ function createBudgetTracker(maxUsd, options) {
789
+ if (maxUsd <= 0) {
790
+ throw new Error(`[bashkit] maxUsd must be positive, got ${maxUsd}`);
791
+ }
792
+ const overrideMap = options?.modelPricing ? new Map(Object.entries(options.modelPricing).map(([k, v]) => [
793
+ k.toLowerCase(),
794
+ v
795
+ ])) : undefined;
796
+ const openRouterPricing = options?.openRouterPricing;
797
+ const onUnpricedModel = options?.onUnpricedModel;
798
+ const warnedModels = new Set;
799
+ const pricingCache = new Map;
800
+ let totalCostUsd = 0;
801
+ let stepsCompleted = 0;
802
+ let unpricedSteps = 0;
803
+ const tracker = {
804
+ onStepFinish(step) {
805
+ const modelId = step.response?.modelId;
806
+ if (!modelId) {
807
+ unpricedSteps++;
808
+ stepsCompleted++;
809
+ return;
810
+ }
811
+ let pricing;
812
+ if (pricingCache.has(modelId)) {
813
+ pricing = pricingCache.get(modelId);
814
+ } else {
815
+ pricing = findPricingForModel(modelId, {
816
+ overrides: overrideMap,
817
+ openRouterCache: openRouterPricing,
818
+ warnedModels
819
+ });
820
+ pricingCache.set(modelId, pricing);
821
+ }
822
+ if (!pricing) {
823
+ unpricedSteps++;
824
+ onUnpricedModel?.(modelId);
825
+ stepsCompleted++;
826
+ return;
827
+ }
828
+ const cost = calculateStepCost(step.usage, pricing);
829
+ totalCostUsd += cost;
830
+ stepsCompleted++;
831
+ },
832
+ stopWhen(_options) {
833
+ return totalCostUsd >= maxUsd;
834
+ },
835
+ getStatus() {
836
+ const remaining = Math.max(0, maxUsd - totalCostUsd);
837
+ return {
838
+ totalCostUsd,
839
+ maxUsd,
840
+ remainingUsd: remaining,
841
+ usagePercent: totalCostUsd / maxUsd * 100,
842
+ stepsCompleted,
843
+ exceeded: totalCostUsd >= maxUsd,
844
+ unpricedSteps
845
+ };
846
+ }
847
+ };
848
+ return tracker;
849
+ }
850
+
615
851
  // src/tools/ask-user.ts
616
852
  import { tool, zodSchema } from "ai";
617
853
  import { z } from "zod";
@@ -2339,7 +2575,8 @@ function createTaskTool(config) {
2339
2575
  subagentTypes = {},
2340
2576
  defaultStopWhen,
2341
2577
  defaultOnStepFinish,
2342
- streamWriter
2578
+ streamWriter,
2579
+ budget
2343
2580
  } = config;
2344
2581
  return tool13({
2345
2582
  description: TASK_DESCRIPTION,
@@ -2367,12 +2604,14 @@ function createTaskTool(config) {
2367
2604
  const model = typeConfig.model || defaultModel;
2368
2605
  const tools = filterTools(allTools, customTools ?? typeConfig.tools, typeConfig.additionalTools);
2369
2606
  const systemPrompt = system_prompt ?? typeConfig.systemPrompt;
2607
+ const baseStopWhen = typeConfig.stopWhen ?? defaultStopWhen ?? stepCountIs(15);
2608
+ const effectiveStopWhen = budget ? [baseStopWhen, budget.stopWhen].flat() : baseStopWhen;
2370
2609
  const commonOptions = {
2371
2610
  model,
2372
2611
  tools,
2373
2612
  system: systemPrompt,
2374
2613
  prompt,
2375
- stopWhen: typeConfig.stopWhen ?? defaultStopWhen ?? stepCountIs(15),
2614
+ stopWhen: effectiveStopWhen,
2376
2615
  prepareStep: typeConfig.prepareStep
2377
2616
  };
2378
2617
  if (streamWriter) {
@@ -2389,6 +2628,7 @@ function createTaskTool(config) {
2389
2628
  const result2 = streamText({
2390
2629
  ...commonOptions,
2391
2630
  onStepFinish: async (step) => {
2631
+ budget?.onStepFinish(step);
2392
2632
  if (step.toolCalls?.length) {
2393
2633
  for (const tc of step.toolCalls) {
2394
2634
  const eventId = generateEventId();
@@ -2463,6 +2703,7 @@ function createTaskTool(config) {
2463
2703
  const result = await generateText2({
2464
2704
  ...commonOptions,
2465
2705
  onStepFinish: async (step) => {
2706
+ budget?.onStepFinish(step);
2466
2707
  await typeConfig.onStepFinish?.(step);
2467
2708
  await defaultOnStepFinish?.({
2468
2709
  subagentType: subagent_type,
@@ -2648,7 +2889,7 @@ function resolveCache(config) {
2648
2889
  enabled
2649
2890
  };
2650
2891
  }
2651
- function createAgentTools(sandbox, config) {
2892
+ async function createAgentTools(sandbox, config) {
2652
2893
  const toolsConfig = {
2653
2894
  ...DEFAULT_CONFIG.tools,
2654
2895
  ...config?.tools
@@ -2698,7 +2939,28 @@ function createAgentTools(sandbox, config) {
2698
2939
  }
2699
2940
  }
2700
2941
  }
2701
- return { tools, planModeState };
2942
+ let budget;
2943
+ 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
+ }
2957
+ }
2958
+ budget = createBudgetTracker(maxUsd, {
2959
+ modelPricing,
2960
+ openRouterPricing
2961
+ });
2962
+ }
2963
+ return { tools, planModeState, budget };
2702
2964
  }
2703
2965
  // src/utils/compact-conversation.ts
2704
2966
  import { generateText as generateText3 } from "ai";
@@ -3418,6 +3680,7 @@ export {
3418
3680
  createEditTool,
3419
3681
  createE2BSandbox,
3420
3682
  createCompactConfig,
3683
+ createBudgetTracker,
3421
3684
  createBashTool,
3422
3685
  createAskUserTool,
3423
3686
  createAgentTools,
@@ -1,6 +1,7 @@
1
1
  import type { ToolSet } from "ai";
2
2
  import type { Sandbox } from "../sandbox/interface";
3
3
  import type { AgentConfig } from "../types";
4
+ import { type BudgetTracker } from "../utils/budget-tracking";
4
5
  import { type PlanModeState } from "./enter-plan-mode";
5
6
  /**
6
7
  * Result from createAgentTools including tools and optional shared state.
@@ -10,6 +11,8 @@ export interface AgentToolsResult {
10
11
  tools: ToolSet;
11
12
  /** Shared plan mode state (only present when planMode is enabled) */
12
13
  planModeState?: PlanModeState;
14
+ /** Budget tracker (only present when budget config is set) */
15
+ budget?: BudgetTracker;
13
16
  }
14
17
  /**
15
18
  * Creates agent tools for AI SDK's generateText/streamText.
@@ -30,23 +33,22 @@ export interface AgentToolsResult {
30
33
  *
31
34
  * @example
32
35
  * // Basic usage (lean default for background agents)
33
- * const { tools } = createAgentTools(sandbox);
36
+ * const { tools } = await createAgentTools(sandbox);
34
37
  *
35
38
  * @example
36
39
  * // Interactive agent with plan mode
37
- * const { tools, planModeState } = createAgentTools(sandbox, {
40
+ * const { tools, planModeState } = await createAgentTools(sandbox, {
38
41
  * planMode: true,
39
42
  * askUser: { onQuestion: async (q) => await promptUser(q) },
40
43
  * });
41
44
  *
42
45
  * @example
43
- * // With web and skill tools
44
- * const { tools } = createAgentTools(sandbox, {
45
- * webSearch: { apiKey: process.env.PARALLEL_API_KEY },
46
- * skill: { skills: discoveredSkills },
46
+ * // With budget tracking (OpenRouter pricing)
47
+ * const { tools, budget } = await createAgentTools(sandbox, {
48
+ * budget: { maxUsd: 5.00, pricingProvider: "openRouter" },
47
49
  * });
48
50
  */
49
- export declare function createAgentTools(sandbox: Sandbox, config?: AgentConfig): AgentToolsResult;
51
+ export declare function createAgentTools(sandbox: Sandbox, config?: AgentConfig): Promise<AgentToolsResult>;
50
52
  export type { AskUserError, AskUserOutput, AskUserResponseHandler, QuestionOption, StructuredQuestion, } from "./ask-user";
51
53
  export { createAskUserTool } from "./ask-user";
52
54
  export type { BashError, BashOutput } from "./bash";
@@ -1,4 +1,5 @@
1
1
  import { type ModelMessage, type LanguageModel, type PrepareStepFunction, type StepResult, type StopCondition, type UIMessageStreamWriter, type Tool, type ToolSet } from "ai";
2
+ import type { BudgetTracker } from "../utils/budget-tracking";
2
3
  import { z } from "zod";
3
4
  export interface TaskOutput {
4
5
  result: string;
@@ -65,6 +66,8 @@ export interface TaskToolConfig {
65
66
  defaultOnStepFinish?: (event: SubagentStepEvent) => void | Promise<void>;
66
67
  /** Optional stream writer for real-time subagent activity (uses streamText instead of generateText) */
67
68
  streamWriter?: UIMessageStreamWriter;
69
+ /** Budget tracker — auto-wires stopWhen and onStepFinish for sub-agent cost tracking */
70
+ budget?: BudgetTracker;
68
71
  }
69
72
  export declare function createTaskTool(config: TaskToolConfig): Tool<TaskInput, TaskOutput | TaskError>;
70
73
  export {};
package/dist/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { LanguageModel, Tool } from "ai";
2
2
  import type { CacheStore } from "./cache/types";
3
3
  import type { SkillMetadata } from "./skills/types";
4
+ import type { ModelPricing } from "./utils/budget-tracking";
4
5
  /**
5
6
  * SDK tool options picked from the Tool type.
6
7
  * This automatically adapts to the user's installed AI SDK version.
@@ -89,6 +90,24 @@ export type CacheConfig = boolean | CacheStore | {
89
90
  /** Per-tool overrides - any tool name can be enabled/disabled */
90
91
  [toolName: string]: boolean | CacheStore | number | ((toolName: string, key: string) => void) | ((toolName: string, params: unknown) => string) | undefined;
91
92
  };
93
+ /**
94
+ * Supported pricing providers for automatic model cost lookup.
95
+ */
96
+ export type PricingProvider = "openRouter";
97
+ /**
98
+ * Budget tracking configuration.
99
+ * At least one of `pricingProvider` or `modelPricing` must be provided.
100
+ */
101
+ export type BudgetConfig = {
102
+ /** Maximum budget in USD (must be positive) */
103
+ maxUsd: number;
104
+ /** Pricing provider for automatic model cost lookup. Omit to skip fetching. */
105
+ pricingProvider?: PricingProvider;
106
+ /** API key for the pricing provider. Passed as bearer token. */
107
+ apiKey?: string;
108
+ /** Per-model pricing overrides (always highest priority over provider data) */
109
+ modelPricing?: Record<string, ModelPricing>;
110
+ };
92
111
  export type AgentConfig = {
93
112
  tools?: {
94
113
  Bash?: ToolConfig;
@@ -110,6 +129,8 @@ export type AgentConfig = {
110
129
  webFetch?: WebFetchConfig;
111
130
  /** Enable tool result caching */
112
131
  cache?: CacheConfig;
132
+ /** Budget tracking configuration */
133
+ budget?: BudgetConfig;
113
134
  defaultTimeout?: number;
114
135
  workingDirectory?: string;
115
136
  };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Budget tracking for AI agent cost management.
3
+ *
4
+ * Tracks cumulative cost across agentic loop steps and stops generation
5
+ * when a budget is exceeded. Pricing is automatic via OpenRouter's free
6
+ * public API. Model ID matching uses PostHog's proven 3-tier strategy.
7
+ */
8
+ import type { LanguageModelUsage, StepResult, StopCondition, ToolSet } from "ai";
9
+ export interface ModelPricing {
10
+ inputPerToken: number;
11
+ outputPerToken: number;
12
+ cacheReadPerToken?: number;
13
+ cacheWritePerToken?: number;
14
+ }
15
+ export interface BudgetStatus {
16
+ totalCostUsd: number;
17
+ maxUsd: number;
18
+ remainingUsd: number;
19
+ usagePercent: number;
20
+ stepsCompleted: number;
21
+ exceeded: boolean;
22
+ /** Number of steps where pricing was unavailable (cost tracked as $0). */
23
+ unpricedSteps: number;
24
+ }
25
+ export interface BudgetTracker {
26
+ /** Track cost for a completed step. Call from onStepFinish. */
27
+ onStepFinish: (step: StepResult<ToolSet>) => void;
28
+ /** Stop condition — returns true when budget exceeded. Compose with other stopWhen conditions. */
29
+ stopWhen: StopCondition<ToolSet>;
30
+ /** Get current budget status (cost, remaining, etc.) */
31
+ getStatus: () => BudgetStatus;
32
+ }
33
+ /**
34
+ * Fetches model pricing from OpenRouter's public API.
35
+ * Results are cached at module level. Concurrent calls are deduplicated.
36
+ *
37
+ * On failure, throws an error (callers decide whether to rethrow or fall back).
38
+ */
39
+ export declare function fetchOpenRouterPricing(apiKey?: string): Promise<Map<string, ModelPricing>>;
40
+ /**
41
+ * Reset the OpenRouter cache. Primarily for testing.
42
+ * @internal
43
+ */
44
+ export declare function resetOpenRouterCache(): void;
45
+ /**
46
+ * Generates match variants for a model ID.
47
+ * Adapted from PostHog/posthog/nodejs/src/ingestion/ai/costs/cost-model-matching.ts
48
+ */
49
+ export declare function getModelMatchVariants(model: string): string[];
50
+ /**
51
+ * Searches for a model's pricing in a cost map using 3-tier matching.
52
+ *
53
+ * 1. Exact match (case-insensitive key lookup)
54
+ * 2. Longest contained match: response model variant *contains* a cost entry variant
55
+ * 3. Reverse containment: cost entry variant *contains* response model variant
56
+ */
57
+ export declare function searchModelInCosts(model: string, costsMap: Map<string, ModelPricing>): ModelPricing | undefined;
58
+ /**
59
+ * Finds pricing for a model, checking user overrides first then OpenRouter cache.
60
+ * Pass a `warnedModels` set to suppress duplicate warnings per tracker instance.
61
+ */
62
+ export declare function findPricingForModel(model: string, options?: {
63
+ overrides?: Record<string, ModelPricing> | Map<string, ModelPricing>;
64
+ openRouterCache?: Map<string, ModelPricing>;
65
+ warnedModels?: Set<string>;
66
+ }): ModelPricing | undefined;
67
+ /**
68
+ * Calculates the cost of a single step based on token usage and pricing.
69
+ */
70
+ export declare function calculateStepCost(usage: LanguageModelUsage, pricing: ModelPricing): number;
71
+ /**
72
+ * Creates a budget tracker that monitors cumulative cost across agentic loop steps.
73
+ *
74
+ * Pricing is looked up synchronously from a pre-fetched OpenRouter pricing map
75
+ * and/or user-provided overrides. Use `fetchOpenRouterPricing()` to eagerly load
76
+ * the map before creating the tracker (this is what `createAgentTools` does).
77
+ *
78
+ * @param maxUsd - Maximum budget in USD (must be positive)
79
+ * @param options - Pricing sources: user overrides and/or pre-fetched OpenRouter map
80
+ * @returns BudgetTracker instance
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * const openRouterPricing = await fetchOpenRouterPricing();
85
+ * const budget = createBudgetTracker(5.00, { openRouterPricing });
86
+ *
87
+ * const result = await generateText({
88
+ * model,
89
+ * tools,
90
+ * stopWhen: [stepCountIs(50), budget.stopWhen],
91
+ * onStepFinish: (step) => {
92
+ * budget.onStepFinish(step);
93
+ * console.log(budget.getStatus());
94
+ * },
95
+ * });
96
+ * ```
97
+ */
98
+ export declare function createBudgetTracker(maxUsd: number, options?: {
99
+ modelPricing?: Record<string, ModelPricing>;
100
+ openRouterPricing?: Map<string, ModelPricing>;
101
+ onUnpricedModel?: (modelId: string) => void;
102
+ }): BudgetTracker;
@@ -1,3 +1,4 @@
1
+ export { type BudgetStatus, type BudgetTracker, type ModelPricing, createBudgetTracker, } from "./budget-tracking";
1
2
  export { type CompactConversationConfig, type CompactConversationResult, type CompactConversationState, compactConversation, createCompactConfig, MODEL_CONTEXT_LIMITS, type ModelContextLimit, } from "./compact-conversation";
2
3
  export { type ContextMetrics, type ContextStatus, type ContextStatusConfig, type ContextStatusLevel, contextNeedsAttention, contextNeedsCompaction, getContextStatus, } from "./context-status";
3
4
  export { type DebugEvent, clearDebugLogs, getDebugLogs, isDebugEnabled, reinitDebugMode, } from "./debug";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bashkit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Agentic coding tools for the Vercel AI SDK",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -35,6 +35,8 @@
35
35
  "lint:check": "biome lint .",
36
36
  "check": "biome check --write .",
37
37
  "check:ci": "biome check .",
38
+ "link-agents": "bash scripts/link-agents-md.sh",
39
+ "check:agents": "bash scripts/check-agents-md.sh",
38
40
  "prepublishOnly": "bun run build"
39
41
  },
40
42
  "keywords": [