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 +22 -5
- package/README.md +26 -0
- package/dist/cli/init.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +304 -38
- package/dist/tools/ask-user.d.ts +12 -12
- package/dist/tools/bash.d.ts +3 -3
- package/dist/tools/edit.d.ts +1 -1
- package/dist/tools/glob.d.ts +1 -1
- package/dist/tools/grep.d.ts +12 -12
- package/dist/tools/index.d.ts +10 -8
- package/dist/tools/read.d.ts +2 -2
- package/dist/tools/task.d.ts +5 -2
- package/dist/tools/web-search.d.ts +2 -2
- package/dist/types.d.ts +21 -0
- package/dist/utils/budget-tracking.d.ts +102 -0
- package/dist/utils/index.d.ts +1 -0
- package/package.json +9 -2
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
|
|
102
|
-
| `Read` | Read files or list directories | `file_path`, `offset
|
|
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
|
|
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().
|
|
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().
|
|
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).
|
|
804
|
-
multiSelect: z.boolean().
|
|
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().
|
|
808
|
-
questions: z.array(structuredQuestionSchema).min(1).max(4).
|
|
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().
|
|
934
|
-
description: z2.string().
|
|
935
|
-
run_in_background: z2.boolean().
|
|
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().
|
|
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
|
|
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().
|
|
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().
|
|
1407
|
-
glob: z7.string().
|
|
1408
|
-
type: z7.string().
|
|
1409
|
-
output_mode: z7.enum(["content", "files_with_matches", "count"]).
|
|
1410
|
-
"-i": z7.boolean().
|
|
1411
|
-
"-n": z7.boolean().
|
|
1412
|
-
"-B": z7.number().
|
|
1413
|
-
"-A": z7.number().
|
|
1414
|
-
"-C": z7.number().
|
|
1415
|
-
head_limit: z7.number().
|
|
1416
|
-
offset: z7.number().
|
|
1417
|
-
multiline: z7.boolean().
|
|
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
|
|
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
|
|
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
|
|
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().
|
|
1708
|
-
limit: z8.number().
|
|
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()).
|
|
2122
|
-
blocked_domains: z11.array(z11.string()).
|
|
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().
|
|
2287
|
-
tools: z13.array(z13.string()).
|
|
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:
|
|
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
|
-
|
|
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,
|
package/dist/tools/ask-user.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
export interface QuestionOption {
|
|
2
2
|
label: string;
|
|
3
|
-
description
|
|
3
|
+
description: string | null;
|
|
4
4
|
}
|
|
5
5
|
export interface StructuredQuestion {
|
|
6
|
-
header
|
|
6
|
+
header: string | null;
|
|
7
7
|
question: string;
|
|
8
|
-
options
|
|
9
|
-
multiSelect
|
|
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
|
|
44
|
-
questions
|
|
43
|
+
question: string | null;
|
|
44
|
+
questions: {
|
|
45
|
+
header: string | null;
|
|
45
46
|
question: string;
|
|
46
|
-
|
|
47
|
-
options?: {
|
|
47
|
+
options: {
|
|
48
48
|
label: string;
|
|
49
|
-
description
|
|
50
|
-
}[] |
|
|
51
|
-
multiSelect
|
|
52
|
-
}[] |
|
|
49
|
+
description: string | null;
|
|
50
|
+
}[] | null;
|
|
51
|
+
multiSelect: boolean | null;
|
|
52
|
+
}[] | null;
|
|
53
53
|
}, AskUserOutput | AskUserError | AskUserAnswerOutput>;
|
package/dist/tools/bash.d.ts
CHANGED
|
@@ -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
|
|
16
|
-
description
|
|
17
|
-
run_in_background
|
|
15
|
+
timeout: number | null;
|
|
16
|
+
description: string | null;
|
|
17
|
+
run_in_background: boolean | null;
|
|
18
18
|
}, BashOutput | BashError>;
|
package/dist/tools/edit.d.ts
CHANGED
package/dist/tools/glob.d.ts
CHANGED
package/dist/tools/grep.d.ts
CHANGED
|
@@ -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
|
|
32
|
-
glob
|
|
33
|
-
type
|
|
34
|
-
output_mode
|
|
35
|
-
"-i"
|
|
36
|
-
"-n"
|
|
37
|
-
"-B"
|
|
38
|
-
"-A"
|
|
39
|
-
"-C"
|
|
40
|
-
head_limit
|
|
41
|
-
offset
|
|
42
|
-
multiline
|
|
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>;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -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
|
|
44
|
-
* const { tools } = createAgentTools(sandbox, {
|
|
45
|
-
*
|
|
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";
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -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
|
|
24
|
-
limit
|
|
23
|
+
offset: number | null;
|
|
24
|
+
limit: number | null;
|
|
25
25
|
}, ReadOutput>;
|
package/dist/tools/task.d.ts
CHANGED
|
@@ -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.
|
|
21
|
-
tools: z.
|
|
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
|
|
21
|
-
blocked_domains
|
|
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;
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -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
|
+
"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",
|