bashkit 0.3.2 → 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,18 +92,35 @@ 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)
98
113
 
99
114
  | Tool | Purpose | Key Inputs |
100
115
  |------|---------|------------|
101
- | `Bash` | Execute shell commands | `command`, `timeout?`, `description?` |
102
- | `Read` | Read files or list directories | `file_path`, `offset?`, `limit?` |
116
+ | `Bash` | Execute shell commands | `command`, `timeout`, `description` |
117
+ | `Read` | Read files or list directories | `file_path`, `offset`, `limit` |
103
118
  | `Write` | Create/overwrite files | `file_path`, `content` |
104
- | `Edit` | Replace strings in files | `file_path`, `old_string`, `new_string`, `replace_all?` |
105
- | `Glob` | Find files by pattern | `pattern`, `path?` |
106
- | `Grep` | Search file contents | `pattern`, `path?`, `output_mode?`, `-i?`, `-C?` |
119
+ | `Edit` | Replace strings in files | `file_path`, `old_string`, `new_string`, `replace_all` |
120
+ | `Glob` | Find files by pattern | `pattern`, `path` |
121
+ | `Grep` | Search file contents | `pattern`, `path`, `output_mode`, `-i`, `-C` |
122
+
123
+ > **Note on nullable types:** Optional parameters use `T | null` (not `T | undefined`) for OpenAI structured outputs compatibility. AI models should send explicit `null` for parameters they don't want to set. This works with both OpenAI and Anthropic models.
107
124
 
108
125
  ### Optional Tools (via config)
109
126
 
package/README.md CHANGED
@@ -19,6 +19,32 @@ Agentic coding tools for Vercel AI SDK. Give AI agents the ability to execute co
19
19
  - Search the web and fetch URLs
20
20
  - Load skills on-demand via the [Agent Skills](https://agentskills.io) standard
21
21
 
22
+ ## Breaking Changes in v0.4.0
23
+
24
+ ### Nullable Types for OpenAI Compatibility
25
+
26
+ All optional tool parameters now use `.nullable()` instead of `.optional()` in Zod schemas. This change enables compatibility with OpenAI's structured outputs, which require all properties to be in the `required` array.
27
+
28
+ **What changed:**
29
+ - Tool input types changed from `T | undefined` to `T | null`
30
+ - Exported interfaces (`QuestionOption`, `StructuredQuestion`) use `T | null`
31
+ - AI models will send explicit `null` values instead of omitting properties
32
+
33
+ **Migration:**
34
+ ```typescript
35
+ // Before v0.4.0
36
+ const option: QuestionOption = { label: "test", description: undefined };
37
+
38
+ // v0.4.0+
39
+ const option: QuestionOption = { label: "test", description: null };
40
+ ```
41
+
42
+ **Why this matters:**
43
+ - Works with both OpenAI and Anthropic models
44
+ - OpenAI structured outputs require nullable (not optional) fields
45
+ - Anthropic/Claude handles nullable fields correctly
46
+ - The `??` operator handles both `null` and `undefined`, so runtime behavior is unchanged
47
+
22
48
  ## Installation
23
49
 
24
50
  ```bash
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
@@ -3,14 +3,14 @@ export { anthropicPromptCacheMiddleware, anthropicPromptCacheMiddlewareV2, } fro
3
3
  export type { E2BSandboxConfig, LocalSandboxConfig, VercelSandboxConfig, } from "./sandbox";
4
4
  export { createE2BSandbox, createLocalSandbox, createVercelSandbox, ensureSandboxTools, } from "./sandbox";
5
5
  export type { ExecOptions, ExecResult, Sandbox } from "./sandbox/interface";
6
- export type { AgentToolsResult, AskUserError, AskUserOutput, AskUserResponseHandler, 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";
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";
@@ -795,17 +1031,17 @@ function reinitDebugMode() {
795
1031
  // src/tools/ask-user.ts
796
1032
  var questionOptionSchema = z.object({
797
1033
  label: z.string().describe("The display text for this option. Should be concise (1-5 words). Add '(Recommended)' suffix for suggested options."),
798
- description: z.string().optional().describe("Explanation of what this option means or its implications.")
1034
+ description: z.string().nullable().default(null).describe("Explanation of what this option means or its implications.")
799
1035
  });
800
1036
  var structuredQuestionSchema = z.object({
801
- header: z.string().optional().describe("Very short label displayed as a chip/tag (max 12 chars). Examples: 'Auth method', 'Library', 'Approach'."),
1037
+ header: z.string().nullable().default(null).describe("Very short label displayed as a chip/tag (max 12 chars). Examples: 'Auth method', 'Library', 'Approach'."),
802
1038
  question: z.string().describe("The complete question to ask the user. Should be clear and specific."),
803
- options: z.array(questionOptionSchema).min(2).max(4).optional().describe("Available choices for this question. 2-4 options. An 'Other' option is automatically available to users."),
804
- multiSelect: z.boolean().optional().describe("Set to true to allow the user to select multiple options instead of just one.")
1039
+ options: z.array(questionOptionSchema).min(2).max(4).nullable().default(null).describe("Available choices for this question. 2-4 options. An 'Other' option is automatically available to users."),
1040
+ multiSelect: z.boolean().nullable().default(null).describe("Set to true to allow the user to select multiple options instead of just one.")
805
1041
  });
806
1042
  var askUserInputSchema = z.object({
807
- question: z.string().optional().describe("Simple question string (for backward compatibility). Use 'questions' for structured multi-choice."),
808
- questions: z.array(structuredQuestionSchema).min(1).max(4).optional().describe("Structured questions with options (1-4 questions).")
1043
+ question: z.string().nullable().default(null).describe("Simple question string (for backward compatibility). Use 'questions' for structured multi-choice."),
1044
+ questions: z.array(structuredQuestionSchema).min(1).max(4).nullable().default(null).describe("Structured questions with options (1-4 questions).")
809
1045
  });
810
1046
  var ASK_USER_DESCRIPTION = `Use this tool when you need to ask the user questions during execution.
811
1047
 
@@ -930,9 +1166,9 @@ import { tool as tool2, zodSchema as zodSchema2 } from "ai";
930
1166
  import { z as z2 } from "zod";
931
1167
  var bashInputSchema = z2.object({
932
1168
  command: z2.string().describe("The command to execute"),
933
- timeout: z2.number().optional().describe("Optional timeout in milliseconds (max 600000)"),
934
- description: z2.string().optional().describe("Clear, concise description of what this command does in 5-10 words"),
935
- run_in_background: z2.boolean().optional().describe("Set to true to run this command in the background")
1169
+ timeout: z2.number().nullable().default(null).describe("Optional timeout in milliseconds (max 600000)"),
1170
+ description: z2.string().nullable().default(null).describe("Clear, concise description of what this command does in 5-10 words"),
1171
+ run_in_background: z2.boolean().nullable().default(null).describe("Set to true to run this command in the background")
936
1172
  });
937
1173
  var BASH_DESCRIPTION = `Executes a bash command in a persistent shell session with optional timeout.
938
1174
 
@@ -1049,7 +1285,7 @@ var editInputSchema = z3.object({
1049
1285
  file_path: z3.string().describe("The absolute path to the file to modify"),
1050
1286
  old_string: z3.string().describe("The text to replace"),
1051
1287
  new_string: z3.string().describe("The text to replace it with (must be different from old_string)"),
1052
- replace_all: z3.boolean().optional().describe("Replace all occurrences of old_string (default false)")
1288
+ replace_all: z3.boolean().nullable().default(null).describe("Replace all occurrences of old_string (default false)")
1053
1289
  });
1054
1290
  var EDIT_DESCRIPTION = `Performs exact string replacements in files.
1055
1291
 
@@ -1079,8 +1315,9 @@ function createEditTool(sandbox, config) {
1079
1315
  file_path,
1080
1316
  old_string,
1081
1317
  new_string,
1082
- replace_all = false
1318
+ replace_all: rawReplaceAll
1083
1319
  }) => {
1320
+ const replace_all = rawReplaceAll ?? false;
1084
1321
  const startTime = performance.now();
1085
1322
  const debugId = isDebugEnabled() ? debugStart("edit", {
1086
1323
  file_path,
@@ -1330,7 +1567,7 @@ import { tool as tool6, zodSchema as zodSchema6 } from "ai";
1330
1567
  import { z as z6 } from "zod";
1331
1568
  var globInputSchema = z6.object({
1332
1569
  pattern: z6.string().describe('Glob pattern to match files (e.g., "**/*.ts", "src/**/*.js", "*.md")'),
1333
- path: z6.string().optional().describe("Directory to search in (defaults to working directory)")
1570
+ path: z6.string().nullable().default(null).describe("Directory to search in (defaults to working directory)")
1334
1571
  });
1335
1572
  var GLOB_DESCRIPTION = `
1336
1573
  - Fast file pattern matching tool that works with any codebase size
@@ -1351,7 +1588,7 @@ function createGlobTool(sandbox, config) {
1351
1588
  pattern,
1352
1589
  path
1353
1590
  }) => {
1354
- const searchPath = path || ".";
1591
+ const searchPath = path ?? ".";
1355
1592
  const startTime = performance.now();
1356
1593
  const debugId = isDebugEnabled() ? debugStart("glob", { pattern, path: searchPath }) : "";
1357
1594
  if (config?.allowedPaths) {
@@ -1403,18 +1640,18 @@ import { tool as tool7, zodSchema as zodSchema7 } from "ai";
1403
1640
  import { z as z7 } from "zod";
1404
1641
  var grepInputSchema = z7.object({
1405
1642
  pattern: z7.string().describe("The regular expression pattern to search for in file contents"),
1406
- path: z7.string().optional().describe("File or directory to search in (defaults to cwd)"),
1407
- glob: z7.string().optional().describe('Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
1408
- type: z7.string().optional().describe('File type to search (e.g. "js", "py", "rust")'),
1409
- output_mode: z7.enum(["content", "files_with_matches", "count"]).optional().describe('Output mode: "content" shows matching lines, "files_with_matches" shows file paths (default), "count" shows match counts'),
1410
- "-i": z7.boolean().optional().describe("Case insensitive search"),
1411
- "-n": z7.boolean().optional().describe("Show line numbers in output. Requires output_mode: 'content'. Defaults to true."),
1412
- "-B": z7.number().optional().describe("Number of lines to show before each match. Requires output_mode: 'content'."),
1413
- "-A": z7.number().optional().describe("Number of lines to show after each match. Requires output_mode: 'content'."),
1414
- "-C": z7.number().optional().describe("Number of lines to show before and after each match. Requires output_mode: 'content'."),
1415
- head_limit: z7.number().optional().describe("Limit output to first N lines/entries. Works across all output modes. Defaults to 0 (unlimited)."),
1416
- offset: z7.number().optional().describe("Skip first N lines/entries before applying head_limit. Works across all output modes. Defaults to 0."),
1417
- multiline: z7.boolean().optional().describe("Enable multiline mode where patterns can span lines. Default: false.")
1643
+ path: z7.string().nullable().default(null).describe("File or directory to search in (defaults to cwd)"),
1644
+ glob: z7.string().nullable().default(null).describe('Glob pattern to filter files (e.g. "*.js", "*.{ts,tsx}")'),
1645
+ type: z7.string().nullable().default(null).describe('File type to search (e.g. "js", "py", "rust")'),
1646
+ output_mode: z7.enum(["content", "files_with_matches", "count"]).nullable().default(null).describe('Output mode: "content" shows matching lines, "files_with_matches" shows file paths (default), "count" shows match counts'),
1647
+ "-i": z7.boolean().nullable().default(null).describe("Case insensitive search"),
1648
+ "-n": z7.boolean().nullable().default(null).describe("Show line numbers in output. Requires output_mode: 'content'. Defaults to true."),
1649
+ "-B": z7.number().nullable().default(null).describe("Number of lines to show before each match. Requires output_mode: 'content'."),
1650
+ "-A": z7.number().nullable().default(null).describe("Number of lines to show after each match. Requires output_mode: 'content'."),
1651
+ "-C": z7.number().nullable().default(null).describe("Number of lines to show before and after each match. Requires output_mode: 'content'."),
1652
+ head_limit: z7.number().nullable().default(null).describe("Limit output to first N lines/entries. Works across all output modes. Defaults to 0 (unlimited)."),
1653
+ offset: z7.number().nullable().default(null).describe("Skip first N lines/entries before applying head_limit. Works across all output modes. Defaults to 0."),
1654
+ multiline: z7.boolean().nullable().default(null).describe("Enable multiline mode where patterns can span lines. Default: false.")
1418
1655
  });
1419
1656
  var GREP_DESCRIPTION = `A powerful content search tool built on ripgrep with regex support.
1420
1657
 
@@ -1449,15 +1686,17 @@ function createGrepTool(sandbox, config) {
1449
1686
  path,
1450
1687
  glob,
1451
1688
  type,
1452
- output_mode = "files_with_matches",
1689
+ output_mode: rawOutputMode,
1453
1690
  "-i": caseInsensitive,
1454
1691
  "-B": beforeContext,
1455
1692
  "-A": afterContext,
1456
1693
  "-C": context,
1457
1694
  head_limit,
1458
- offset = 0,
1695
+ offset: rawOffset,
1459
1696
  multiline
1460
1697
  } = input;
1698
+ const output_mode = rawOutputMode ?? "files_with_matches";
1699
+ const offset = rawOffset ?? 0;
1461
1700
  const searchPath = path || ".";
1462
1701
  const startTime = performance.now();
1463
1702
  const debugId = isDebugEnabled() ? debugStart("grep", {
@@ -1607,7 +1846,7 @@ function parseCountOutput(stdout) {
1607
1846
  total
1608
1847
  };
1609
1848
  }
1610
- function parseContentOutput(stdout, head_limit, offset = 0) {
1849
+ function parseContentOutput(stdout, head_limit, offset) {
1611
1850
  const fileData = new Map;
1612
1851
  for (const line of stdout.split(`
1613
1852
  `).filter(Boolean)) {
@@ -1704,8 +1943,8 @@ import { tool as tool8, zodSchema as zodSchema8 } from "ai";
1704
1943
  import { z as z8 } from "zod";
1705
1944
  var readInputSchema = z8.object({
1706
1945
  file_path: z8.string().describe("Absolute path to file or directory"),
1707
- offset: z8.number().optional().describe("Line number to start reading from (1-indexed)"),
1708
- limit: z8.number().optional().describe("Maximum number of lines to read")
1946
+ offset: z8.number().nullable().default(null).describe("Line number to start reading from (1-indexed)"),
1947
+ limit: z8.number().nullable().default(null).describe("Maximum number of lines to read")
1709
1948
  });
1710
1949
  var READ_DESCRIPTION = `Reads a file from the local filesystem. You can access any file directly by using this tool.
1711
1950
  Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
@@ -2118,8 +2357,8 @@ async function searchContent(apiKey, provider, options) {
2118
2357
  }
2119
2358
  var webSearchInputSchema = z11.object({
2120
2359
  query: z11.string().describe("The search query to use"),
2121
- allowed_domains: z11.array(z11.string()).optional().describe("Only include results from these domains"),
2122
- blocked_domains: z11.array(z11.string()).optional().describe("Never include results from these domains")
2360
+ allowed_domains: z11.array(z11.string()).nullable().default(null).describe("Only include results from these domains"),
2361
+ blocked_domains: z11.array(z11.string()).nullable().default(null).describe("Never include results from these domains")
2123
2362
  });
2124
2363
  var WEB_SEARCH_DESCRIPTION = `Searches the web and returns results with links. Use this for accessing up-to-date information beyond your knowledge cutoff.
2125
2364
 
@@ -2283,8 +2522,8 @@ var taskInputSchema = z13.object({
2283
2522
  description: z13.string().describe("A short (3-5 word) description of the task"),
2284
2523
  prompt: z13.string().describe("The task for the agent to perform"),
2285
2524
  subagent_type: z13.string().describe("The type of specialized agent to use for this task"),
2286
- system_prompt: z13.string().optional().describe("Optional custom system prompt for this agent. If provided, overrides the default system prompt for the subagent type. Use this to create dynamic, specialized agents on the fly."),
2287
- tools: z13.array(z13.string()).optional().describe("Optional list of tool names this agent can use (e.g., ['Read', 'Grep', 'WebSearch']). If provided, overrides the default tools for the subagent type. Use this to restrict or expand the agent's capabilities.")
2525
+ system_prompt: z13.string().nullable().default(null).describe("Optional custom system prompt for this agent. If provided, overrides the default system prompt for the subagent type. Use this to create dynamic, specialized agents on the fly."),
2526
+ tools: z13.array(z13.string()).nullable().default(null).describe("Optional list of tool names this agent can use (e.g., ['Read', 'Grep', 'WebSearch']). If provided, overrides the default tools for the subagent type. Use this to restrict or expand the agent's capabilities.")
2288
2527
  });
2289
2528
  var TASK_DESCRIPTION = `Launch a new agent to handle complex, multi-step tasks autonomously.
2290
2529
 
@@ -2336,7 +2575,8 @@ function createTaskTool(config) {
2336
2575
  subagentTypes = {},
2337
2576
  defaultStopWhen,
2338
2577
  defaultOnStepFinish,
2339
- streamWriter
2578
+ streamWriter,
2579
+ budget
2340
2580
  } = config;
2341
2581
  return tool13({
2342
2582
  description: TASK_DESCRIPTION,
@@ -2364,12 +2604,14 @@ function createTaskTool(config) {
2364
2604
  const model = typeConfig.model || defaultModel;
2365
2605
  const tools = filterTools(allTools, customTools ?? typeConfig.tools, typeConfig.additionalTools);
2366
2606
  const systemPrompt = system_prompt ?? typeConfig.systemPrompt;
2607
+ const baseStopWhen = typeConfig.stopWhen ?? defaultStopWhen ?? stepCountIs(15);
2608
+ const effectiveStopWhen = budget ? [baseStopWhen, budget.stopWhen].flat() : baseStopWhen;
2367
2609
  const commonOptions = {
2368
2610
  model,
2369
2611
  tools,
2370
2612
  system: systemPrompt,
2371
2613
  prompt,
2372
- stopWhen: typeConfig.stopWhen ?? defaultStopWhen ?? stepCountIs(15),
2614
+ stopWhen: effectiveStopWhen,
2373
2615
  prepareStep: typeConfig.prepareStep
2374
2616
  };
2375
2617
  if (streamWriter) {
@@ -2386,6 +2628,7 @@ function createTaskTool(config) {
2386
2628
  const result2 = streamText({
2387
2629
  ...commonOptions,
2388
2630
  onStepFinish: async (step) => {
2631
+ budget?.onStepFinish(step);
2389
2632
  if (step.toolCalls?.length) {
2390
2633
  for (const tc of step.toolCalls) {
2391
2634
  const eventId = generateEventId();
@@ -2460,6 +2703,7 @@ function createTaskTool(config) {
2460
2703
  const result = await generateText2({
2461
2704
  ...commonOptions,
2462
2705
  onStepFinish: async (step) => {
2706
+ budget?.onStepFinish(step);
2463
2707
  await typeConfig.onStepFinish?.(step);
2464
2708
  await defaultOnStepFinish?.({
2465
2709
  subagentType: subagent_type,
@@ -2645,7 +2889,7 @@ function resolveCache(config) {
2645
2889
  enabled
2646
2890
  };
2647
2891
  }
2648
- function createAgentTools(sandbox, config) {
2892
+ async function createAgentTools(sandbox, config) {
2649
2893
  const toolsConfig = {
2650
2894
  ...DEFAULT_CONFIG.tools,
2651
2895
  ...config?.tools
@@ -2695,7 +2939,28 @@ function createAgentTools(sandbox, config) {
2695
2939
  }
2696
2940
  }
2697
2941
  }
2698
- 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 };
2699
2964
  }
2700
2965
  // src/utils/compact-conversation.ts
2701
2966
  import { generateText as generateText3 } from "ai";
@@ -3415,6 +3680,7 @@ export {
3415
3680
  createEditTool,
3416
3681
  createE2BSandbox,
3417
3682
  createCompactConfig,
3683
+ createBudgetTracker,
3418
3684
  createBashTool,
3419
3685
  createAskUserTool,
3420
3686
  createAgentTools,
@@ -1,12 +1,12 @@
1
1
  export interface QuestionOption {
2
2
  label: string;
3
- description?: string;
3
+ description: string | null;
4
4
  }
5
5
  export interface StructuredQuestion {
6
- header?: string;
6
+ header: string | null;
7
7
  question: string;
8
- options?: QuestionOption[];
9
- multiSelect?: boolean;
8
+ options: QuestionOption[] | null;
9
+ multiSelect: boolean | null;
10
10
  }
11
11
  export interface AskUserSimpleOutput {
12
12
  question: string;
@@ -40,14 +40,14 @@ export interface AskUserToolConfig {
40
40
  * @param config - Configuration with optional handlers for questions
41
41
  */
42
42
  export declare function createAskUserTool(config?: AskUserToolConfig | AskUserResponseHandler): import("ai").Tool<{
43
- question?: string | undefined;
44
- questions?: {
43
+ question: string | null;
44
+ questions: {
45
+ header: string | null;
45
46
  question: string;
46
- header?: string | undefined;
47
- options?: {
47
+ options: {
48
48
  label: string;
49
- description?: string | undefined;
50
- }[] | undefined;
51
- multiSelect?: boolean | undefined;
52
- }[] | undefined;
49
+ description: string | null;
50
+ }[] | null;
51
+ multiSelect: boolean | null;
52
+ }[] | null;
53
53
  }, AskUserOutput | AskUserError | AskUserAnswerOutput>;
@@ -12,7 +12,7 @@ export interface BashError {
12
12
  }
13
13
  export declare function createBashTool(sandbox: Sandbox, config?: ToolConfig): import("ai").Tool<{
14
14
  command: string;
15
- timeout?: number | undefined;
16
- description?: string | undefined;
17
- run_in_background?: boolean | undefined;
15
+ timeout: number | null;
16
+ description: string | null;
17
+ run_in_background: boolean | null;
18
18
  }, BashOutput | BashError>;
@@ -12,5 +12,5 @@ export declare function createEditTool(sandbox: Sandbox, config?: ToolConfig): i
12
12
  file_path: string;
13
13
  old_string: string;
14
14
  new_string: string;
15
- replace_all?: boolean | undefined;
15
+ replace_all: boolean | null;
16
16
  }, EditOutput | EditError>;
@@ -10,5 +10,5 @@ export interface GlobError {
10
10
  }
11
11
  export declare function createGlobTool(sandbox: Sandbox, config?: ToolConfig): import("ai").Tool<{
12
12
  pattern: string;
13
- path?: string | undefined;
13
+ path: string | null;
14
14
  }, GlobOutput | GlobError>;
@@ -28,16 +28,16 @@ export interface GrepError {
28
28
  export type GrepOutput = GrepContentOutput | GrepFilesOutput | GrepCountOutput | GrepError;
29
29
  export declare function createGrepTool(sandbox: Sandbox, config?: GrepToolConfig): import("ai").Tool<{
30
30
  pattern: string;
31
- path?: string | undefined;
32
- glob?: string | undefined;
33
- type?: string | undefined;
34
- output_mode?: "content" | "count" | "files_with_matches" | undefined;
35
- "-i"?: boolean | undefined;
36
- "-n"?: boolean | undefined;
37
- "-B"?: number | undefined;
38
- "-A"?: number | undefined;
39
- "-C"?: number | undefined;
40
- head_limit?: number | undefined;
41
- offset?: number | undefined;
42
- multiline?: boolean | undefined;
31
+ path: string | null;
32
+ glob: string | null;
33
+ type: string | null;
34
+ output_mode: "content" | "count" | "files_with_matches" | null;
35
+ "-i": boolean | null;
36
+ "-n": boolean | null;
37
+ "-B": number | null;
38
+ "-A": number | null;
39
+ "-C": number | null;
40
+ head_limit: number | null;
41
+ offset: number | null;
42
+ multiline: boolean | null;
43
43
  }, GrepOutput>;
@@ -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,24 +33,23 @@ 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;
50
- export type { AskUserError, AskUserOutput, AskUserResponseHandler, } from "./ask-user";
51
+ export declare function createAgentTools(sandbox: Sandbox, config?: AgentConfig): Promise<AgentToolsResult>;
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";
53
55
  export { createBashTool } from "./bash";
@@ -20,6 +20,6 @@ export interface ReadError {
20
20
  export type ReadOutput = ReadTextOutput | ReadDirectoryOutput | ReadError;
21
21
  export declare function createReadTool(sandbox: Sandbox, config?: ToolConfig): import("ai").Tool<{
22
22
  file_path: string;
23
- offset?: number | undefined;
24
- limit?: number | undefined;
23
+ offset: number | null;
24
+ limit: number | null;
25
25
  }, ReadOutput>;
@@ -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;
@@ -17,8 +18,8 @@ declare const taskInputSchema: z.ZodObject<{
17
18
  description: z.ZodString;
18
19
  prompt: z.ZodString;
19
20
  subagent_type: z.ZodString;
20
- system_prompt: z.ZodOptional<z.ZodString>;
21
- tools: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ system_prompt: z.ZodDefault<z.ZodNullable<z.ZodString>>;
22
+ tools: z.ZodDefault<z.ZodNullable<z.ZodArray<z.ZodString>>>;
22
23
  }, z.core.$strip>;
23
24
  type TaskInput = z.infer<typeof taskInputSchema>;
24
25
  /** Event emitted for each step a subagent takes */
@@ -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 {};
@@ -17,6 +17,6 @@ export interface WebSearchError {
17
17
  }
18
18
  export declare function createWebSearchTool(config: WebSearchConfig): import("ai").Tool<{
19
19
  query: string;
20
- allowed_domains?: string[] | undefined;
21
- blocked_domains?: string[] | undefined;
20
+ allowed_domains: string[] | null;
21
+ blocked_domains: string[] | null;
22
22
  }, WebSearchOutput | WebSearchError>;
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.3.2",
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",
@@ -26,12 +26,17 @@
26
26
  "build:cli": "bun build src/cli/init.ts --outdir dist/cli --target node --format esm --external @clack/prompts && chmod +x dist/cli/init.js",
27
27
  "build:types": "tsc -p tsconfig.build.json",
28
28
  "typecheck": "tsc --noEmit",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest watch",
31
+ "test:coverage": "vitest run --coverage",
29
32
  "format": "biome format --write .",
30
33
  "format:check": "biome format .",
31
34
  "lint": "biome lint --write .",
32
35
  "lint:check": "biome lint .",
33
36
  "check": "biome check --write .",
34
37
  "check:ci": "biome check .",
38
+ "link-agents": "bash scripts/link-agents-md.sh",
39
+ "check:agents": "bash scripts/check-agents-md.sh",
35
40
  "prepublishOnly": "bun run build"
36
41
  },
37
42
  "keywords": [
@@ -61,9 +66,11 @@
61
66
  "@types/bun": "latest",
62
67
  "@types/node": "^24.10.0",
63
68
  "@vercel/sandbox": "^1.0.4",
69
+ "@vitest/coverage-v8": "^3.0.0",
64
70
  "ai": "^6.0.0",
65
71
  "parallel-web": "^0.2.4",
66
- "typescript": "^5.9.3"
72
+ "typescript": "^5.9.3",
73
+ "vitest": "^3.0.0"
67
74
  },
68
75
  "peerDependencies": {
69
76
  "ai": ">=5.0.0",