brass-runtime 1.20.0 → 1.21.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.
@@ -12,6 +12,7 @@ import {
12
12
  asyncFail,
13
13
  asyncFlatMap,
14
14
  asyncFold,
15
+ asyncInterruptible,
15
16
  asyncMap,
16
17
  asyncSucceed,
17
18
  asyncSync
@@ -62,6 +63,187 @@ var reduceAgentState = (state, observation) => {
62
63
  };
63
64
  var isTerminal = (state) => state.phase === "done" || state.phase === "failed" || state.steps >= MAX_AGENT_STEPS;
64
65
 
66
+ // src/agent/core/contextBudget/armExtraction.ts
67
+ var FALLBACK_ARM_ID = "__fallback__";
68
+ var COMPOUND_EXTENSIONS = [
69
+ ".test.ts",
70
+ ".test.tsx",
71
+ ".test.js",
72
+ ".test.jsx",
73
+ ".spec.ts",
74
+ ".spec.tsx",
75
+ ".spec.js",
76
+ ".spec.jsx",
77
+ ".pbt.test.ts",
78
+ ".pbt.test.js",
79
+ ".d.ts",
80
+ ".d.mts",
81
+ ".d.cts",
82
+ ".config.ts",
83
+ ".config.js",
84
+ ".config.mjs",
85
+ ".config.cjs",
86
+ ".module.ts",
87
+ ".module.css",
88
+ ".stories.tsx",
89
+ ".stories.ts"
90
+ ];
91
+ var extractExtension = (filename) => {
92
+ const lower = filename.toLowerCase();
93
+ for (const ext of COMPOUND_EXTENSIONS) {
94
+ if (lower.endsWith(ext) && filename.length > ext.length) {
95
+ return filename.slice(filename.length - ext.length);
96
+ }
97
+ }
98
+ const dotIndex = filename.lastIndexOf(".");
99
+ if (dotIndex > 0 && dotIndex < filename.length - 1) {
100
+ return filename.slice(dotIndex);
101
+ }
102
+ return "";
103
+ };
104
+ var normalizePath = (filePath) => filePath.replace(/\\/g, "/");
105
+ var deriveArmId = (filePath) => {
106
+ try {
107
+ if (!filePath || typeof filePath !== "string") {
108
+ return FALLBACK_ARM_ID;
109
+ }
110
+ const normalized = normalizePath(filePath.trim());
111
+ if (!normalized) {
112
+ return FALLBACK_ARM_ID;
113
+ }
114
+ const lastSlash = normalized.lastIndexOf("/");
115
+ const directory = lastSlash >= 0 ? normalized.slice(0, lastSlash) : "";
116
+ const filename = lastSlash >= 0 ? normalized.slice(lastSlash + 1) : normalized;
117
+ if (!filename) {
118
+ return FALLBACK_ARM_ID;
119
+ }
120
+ const extension = extractExtension(filename);
121
+ if (!extension) {
122
+ return FALLBACK_ARM_ID;
123
+ }
124
+ const glob = `*${extension}`;
125
+ return directory ? `${directory}/${glob}` : glob;
126
+ } catch {
127
+ return FALLBACK_ARM_ID;
128
+ }
129
+ };
130
+ var assignArm = (filePath) => {
131
+ const id = deriveArmId(filePath);
132
+ return { id, pattern: id };
133
+ };
134
+ var groupByArm = (candidates) => {
135
+ const map = /* @__PURE__ */ new Map();
136
+ for (const path of candidates) {
137
+ const arm = assignArm(path);
138
+ const existing = map.get(arm.id);
139
+ if (existing) {
140
+ existing.push(path);
141
+ } else {
142
+ map.set(arm.id, [path]);
143
+ }
144
+ }
145
+ return map;
146
+ };
147
+
148
+ // src/agent/core/contextBudget/banditEngine.ts
149
+ var defaultArmStats = () => ({
150
+ alpha: 1,
151
+ beta: 1,
152
+ pulls: 0,
153
+ lastPulledAt: 0
154
+ });
155
+ var sampleBeta = (alpha, beta, rng) => {
156
+ const a = Math.max(alpha, 1e-3);
157
+ const b = Math.max(beta, 1e-3);
158
+ const x = sampleGamma(a, rng);
159
+ const y = sampleGamma(b, rng);
160
+ if (x + y === 0) return 0.5;
161
+ return x / (x + y);
162
+ };
163
+ var sampleGamma = (shape, rng) => {
164
+ if (shape < 1) {
165
+ const sample = sampleGamma(shape + 1, rng);
166
+ const u = rng();
167
+ return sample * Math.pow(u === 0 ? 1e-10 : u, 1 / shape);
168
+ }
169
+ const d = shape - 1 / 3;
170
+ const c = 1 / Math.sqrt(9 * d);
171
+ for (; ; ) {
172
+ let x;
173
+ let v;
174
+ do {
175
+ const u1 = rng();
176
+ const u2 = rng();
177
+ x = Math.sqrt(-2 * Math.log(u1 === 0 ? 1e-10 : u1)) * Math.cos(2 * Math.PI * u2);
178
+ v = 1 + c * x;
179
+ } while (v <= 0);
180
+ v = v * v * v;
181
+ const u = rng();
182
+ if (u < 1 - 0.0331 * (x * x) * (x * x)) {
183
+ return d * v;
184
+ }
185
+ if (Math.log(u === 0 ? 1e-10 : u) < 0.5 * x * x + d * (1 - v + Math.log(v))) {
186
+ return d * v;
187
+ }
188
+ }
189
+ };
190
+ var selectArms = (state, candidateArms, rng) => {
191
+ if (candidateArms.length === 0) return [];
192
+ const sampled = candidateArms.map((arm) => {
193
+ const stats = state.arms[arm.id] ?? defaultArmStats();
194
+ const value = sampleBeta(stats.alpha, stats.beta, rng);
195
+ return { arm, value };
196
+ });
197
+ sampled.sort((a, b) => b.value - a.value);
198
+ return sampled.map((s) => s.arm);
199
+ };
200
+
201
+ // src/agent/core/contextBudget/integration.ts
202
+ var shouldApplyBandit = (state, contextEnabled, hasInitialPatch) => {
203
+ if (Object.keys(state.arms).length === 0) return false;
204
+ if (!contextEnabled) return false;
205
+ if (hasInitialPatch) return false;
206
+ return true;
207
+ };
208
+ var reorderCandidates = (candidates, state, rng) => {
209
+ try {
210
+ if (Object.keys(state.arms).length === 0) {
211
+ return candidates;
212
+ }
213
+ const armGroups = groupByArm(candidates);
214
+ const uniqueArms = [];
215
+ for (const armId of armGroups.keys()) {
216
+ uniqueArms.push({ id: armId, pattern: armId });
217
+ }
218
+ const prioritizedArms = selectArms(state, uniqueArms, rng);
219
+ const result = [];
220
+ const includedArmIds = /* @__PURE__ */ new Set();
221
+ for (const arm of prioritizedArms) {
222
+ const paths = armGroups.get(arm.id);
223
+ if (paths) {
224
+ for (const p of paths) {
225
+ result.push(p);
226
+ }
227
+ includedArmIds.add(arm.id);
228
+ }
229
+ }
230
+ for (const [armId, paths] of armGroups) {
231
+ if (!includedArmIds.has(armId)) {
232
+ for (const p of paths) {
233
+ result.push(p);
234
+ }
235
+ }
236
+ }
237
+ return result;
238
+ } catch (error) {
239
+ console.warn(
240
+ "[contextBudget] reorderCandidates failed, returning original order:",
241
+ error instanceof Error ? error.message : String(error)
242
+ );
243
+ return candidates;
244
+ }
245
+ };
246
+
65
247
  // src/agent/core/contextDiscovery.ts
66
248
  var DEFAULT_CONTEXT_GLOBS = [
67
249
  "*.ts",
@@ -359,7 +541,7 @@ var summarizeContextDiscovery = (state) => {
359
541
  remainingFileBudget
360
542
  };
361
543
  };
362
- var nextContextDiscoveryAction = (state) => {
544
+ var nextContextDiscoveryAction = (state, banditState) => {
363
545
  const config = configFor(state);
364
546
  if (!config.enabled) return void 0;
365
547
  if (state.goal.initialPatch?.trim()) return void 0;
@@ -367,8 +549,14 @@ var nextContextDiscoveryAction = (state) => {
367
549
  const readsRemaining = Math.max(0, config.maxFiles - contextFileReadCount(state));
368
550
  const directPaths = extractLikelyFilePaths(state);
369
551
  const resultPaths = pathsFromSearchResults(state, config.maxSearchResults);
552
+ const allCandidates = [...directPaths, ...resultPaths];
553
+ const orderedCandidates = banditState && shouldApplyBandit(
554
+ banditState,
555
+ config.enabled,
556
+ Boolean(state.goal.initialPatch?.trim())
557
+ ) ? reorderCandidates(allCandidates, banditState, Math.random) : allCandidates;
370
558
  if (readsRemaining > 0) {
371
- const nextPath = [...directPaths, ...resultPaths].find(
559
+ const nextPath = orderedCandidates.find(
372
560
  (path) => path !== "package.json" && !alreadyRead(state, path) && !knownMissing(state, path)
373
561
  );
374
562
  if (nextPath) {
@@ -1018,6 +1206,343 @@ var describeLanguagePolicy = (goal) => {
1018
1206
  };
1019
1207
  var spanishLike = (goal) => responseLanguageName(goal) === "Spanish";
1020
1208
 
1209
+ // src/agent/core/patchStrategy/types.ts
1210
+ var PATCH_STRATEGIES = [
1211
+ "direct-patch",
1212
+ "multi-step-patch",
1213
+ "propose-then-refine"
1214
+ ];
1215
+ var DEFAULT_STRATEGY = "direct-patch";
1216
+
1217
+ // src/agent/core/patchStrategy/thompson.ts
1218
+ var initialThompsonState = () => ({
1219
+ algorithm: "thompson",
1220
+ arms: {
1221
+ "direct-patch": { alpha: 1, beta: 1 },
1222
+ "multi-step-patch": { alpha: 1, beta: 1 },
1223
+ "propose-then-refine": { alpha: 1, beta: 1 }
1224
+ }
1225
+ });
1226
+ var thompsonSelect = (state, rng) => {
1227
+ let bestArm = PATCH_STRATEGIES[0];
1228
+ let bestSample = -Infinity;
1229
+ for (const arm of PATCH_STRATEGIES) {
1230
+ const { alpha, beta } = state.arms[arm];
1231
+ const sample = rng.sampleBeta(alpha, beta);
1232
+ if (sample > bestSample) {
1233
+ bestSample = sample;
1234
+ bestArm = arm;
1235
+ }
1236
+ }
1237
+ return bestArm;
1238
+ };
1239
+ var thompsonUpdate = (state, arm, reward) => {
1240
+ const clampedReward = Math.max(0, Math.min(1, reward));
1241
+ const current = state.arms[arm];
1242
+ const newAlpha = Math.max(1, current.alpha + clampedReward);
1243
+ const newBeta = Math.max(1, current.beta + (1 - clampedReward));
1244
+ return {
1245
+ ...state,
1246
+ arms: {
1247
+ ...state.arms,
1248
+ [arm]: { alpha: newAlpha, beta: newBeta }
1249
+ }
1250
+ };
1251
+ };
1252
+ var thompsonStateFromHistory = (history) => {
1253
+ let state = initialThompsonState();
1254
+ for (const entry of history) {
1255
+ state = thompsonUpdate(state, entry.arm, entry.reward);
1256
+ }
1257
+ return state;
1258
+ };
1259
+
1260
+ // src/agent/core/patchStrategy/exp3.ts
1261
+ var K = 3;
1262
+ var initialEXP3State = (gamma = 0.3) => ({
1263
+ algorithm: "exp3",
1264
+ arms: {
1265
+ "direct-patch": { weight: 1 },
1266
+ "multi-step-patch": { weight: 1 },
1267
+ "propose-then-refine": { weight: 1 }
1268
+ },
1269
+ gamma: Math.max(0, Math.min(1, gamma)) || 0.3,
1270
+ totalRounds: 0
1271
+ });
1272
+ var exp3Probabilities = (state) => {
1273
+ const { arms, gamma } = state;
1274
+ let weightSum = 0;
1275
+ for (const arm of PATCH_STRATEGIES) {
1276
+ weightSum += arms[arm].weight;
1277
+ }
1278
+ const probs = {};
1279
+ let probSum = 0;
1280
+ for (const arm of PATCH_STRATEGIES) {
1281
+ const p = (1 - gamma) * (arms[arm].weight / weightSum) + gamma / K;
1282
+ probs[arm] = p;
1283
+ probSum += p;
1284
+ }
1285
+ for (const arm of PATCH_STRATEGIES) {
1286
+ probs[arm] /= probSum;
1287
+ }
1288
+ return probs;
1289
+ };
1290
+ var exp3Select = (state, rng) => {
1291
+ const probs = exp3Probabilities(state);
1292
+ const r = rng.random();
1293
+ let cumulative = 0;
1294
+ for (const arm of PATCH_STRATEGIES) {
1295
+ cumulative += probs[arm];
1296
+ if (r < cumulative) {
1297
+ return arm;
1298
+ }
1299
+ }
1300
+ return PATCH_STRATEGIES[PATCH_STRATEGIES.length - 1];
1301
+ };
1302
+ var exp3Update = (state, arm, reward) => {
1303
+ const clampedReward = Math.max(0, Math.min(1, reward));
1304
+ const probs = exp3Probabilities(state);
1305
+ const pSelected = probs[arm];
1306
+ const estimatedReward = clampedReward / pSelected;
1307
+ const currentWeight = state.arms[arm].weight;
1308
+ const newWeight = Math.min(
1309
+ currentWeight * Math.exp(state.gamma * estimatedReward / K),
1310
+ 1e100
1311
+ );
1312
+ return {
1313
+ ...state,
1314
+ arms: {
1315
+ ...state.arms,
1316
+ [arm]: { weight: newWeight }
1317
+ },
1318
+ totalRounds: state.totalRounds + 1
1319
+ };
1320
+ };
1321
+ var exp3StateFromHistory = (history, gamma = 0.3) => {
1322
+ let state = initialEXP3State(gamma);
1323
+ for (const entry of history) {
1324
+ state = exp3Update(state, entry.arm, entry.reward);
1325
+ }
1326
+ return state;
1327
+ };
1328
+
1329
+ // src/agent/core/patchStrategy/selector.ts
1330
+ var selectStrategy = (_signals, config, history, rng) => {
1331
+ if (config?.enabled === false) {
1332
+ return DEFAULT_STRATEGY;
1333
+ }
1334
+ if (history.length === 0) {
1335
+ return DEFAULT_STRATEGY;
1336
+ }
1337
+ const algorithm = config?.algorithm ?? "thompson";
1338
+ switch (algorithm) {
1339
+ case "thompson": {
1340
+ const state = thompsonStateFromHistory(history);
1341
+ return thompsonSelect(state, rng);
1342
+ }
1343
+ case "exp3": {
1344
+ const rawGamma = config?.gamma ?? 0.3;
1345
+ const gamma = Math.max(Number.EPSILON, Math.min(1, rawGamma)) || 0.3;
1346
+ const state = exp3StateFromHistory(history, gamma);
1347
+ return exp3Select(state, rng);
1348
+ }
1349
+ default: {
1350
+ const state = thompsonStateFromHistory(history);
1351
+ return thompsonSelect(state, rng);
1352
+ }
1353
+ }
1354
+ };
1355
+
1356
+ // src/agent/core/patchStrategy/signalExtractor.ts
1357
+ var FILE_EXTENSIONS = [
1358
+ "tsx",
1359
+ "jsx",
1360
+ "mts",
1361
+ "cts",
1362
+ "mjs",
1363
+ "cjs",
1364
+ "json",
1365
+ "yaml",
1366
+ "html",
1367
+ "scss",
1368
+ "ts",
1369
+ "js",
1370
+ "md",
1371
+ "yml",
1372
+ "css"
1373
+ ];
1374
+ var ASCII_CASE_BIT = 32;
1375
+ var MAX_FILE_PATH_CANDIDATE_LENGTH = 512;
1376
+ var KEYWORDS = [
1377
+ "refactor",
1378
+ "rename",
1379
+ "bug",
1380
+ "fix",
1381
+ "add",
1382
+ "create",
1383
+ "move",
1384
+ "delete"
1385
+ ];
1386
+ var KEYWORD_PATTERNS = new Map(
1387
+ KEYWORDS.map((kw) => [kw, new RegExp(`\\b${kw}\\b`, "i")])
1388
+ );
1389
+ var categorizeGoalLength = (text) => {
1390
+ const len = text.length;
1391
+ if (len < 80) return "short";
1392
+ if (len <= 300) return "medium";
1393
+ return "long";
1394
+ };
1395
+ var isAsciiAlpha = (code) => code >= 65 && code <= 90 || code >= 97 && code <= 122;
1396
+ var isAsciiDigit = (code) => code >= 48 && code <= 57;
1397
+ var isPathCandidateChar = (code) => isAsciiAlpha(code) || isAsciiDigit(code) || code === 95 || // _
1398
+ code === 64 || // @
1399
+ code === 46 || // .
1400
+ code === 45 || // -
1401
+ code === 47 || // /
1402
+ code === 92 || // \
1403
+ code === 58;
1404
+ var matchesExtension = (text, start, end, extension) => {
1405
+ if (end - start !== extension.length) return false;
1406
+ for (let offset = 0; offset < extension.length; offset++) {
1407
+ const textCode = text.charCodeAt(start + offset) | ASCII_CASE_BIT;
1408
+ if (textCode !== extension.charCodeAt(offset)) {
1409
+ return false;
1410
+ }
1411
+ }
1412
+ return true;
1413
+ };
1414
+ var matchesKnownExtension = (text, start, end) => {
1415
+ for (const extension of FILE_EXTENSIONS) {
1416
+ if (matchesExtension(text, start, end, extension)) {
1417
+ return true;
1418
+ }
1419
+ }
1420
+ return false;
1421
+ };
1422
+ var stripTrailingPathPunctuation = (text, start, end) => {
1423
+ let currentEnd = end;
1424
+ while (currentEnd > start) {
1425
+ const code = text.charCodeAt(currentEnd - 1);
1426
+ if (code !== 46 && code !== 58) break;
1427
+ currentEnd--;
1428
+ }
1429
+ return currentEnd;
1430
+ };
1431
+ var stripLineSuffixes = (text, start, end) => {
1432
+ let currentEnd = end;
1433
+ for (let suffixCount = 0; suffixCount < 2; suffixCount++) {
1434
+ let cursor = currentEnd - 1;
1435
+ if (cursor < start || !isAsciiDigit(text.charCodeAt(cursor))) break;
1436
+ while (cursor >= start && isAsciiDigit(text.charCodeAt(cursor))) {
1437
+ cursor--;
1438
+ }
1439
+ if (cursor < start || text.charCodeAt(cursor) !== 58) break;
1440
+ currentEnd = cursor;
1441
+ }
1442
+ return currentEnd;
1443
+ };
1444
+ var findLastDot = (text, start, end) => {
1445
+ for (let index = end - 1; index >= start; index--) {
1446
+ if (text.charCodeAt(index) === 46) {
1447
+ return index;
1448
+ }
1449
+ }
1450
+ return -1;
1451
+ };
1452
+ var findFilenameStart = (text, start, dotIndex) => {
1453
+ let filenameStart = start;
1454
+ for (let index = start; index < dotIndex; index++) {
1455
+ const code = text.charCodeAt(index);
1456
+ if (code === 47 || code === 92) {
1457
+ filenameStart = index + 1;
1458
+ }
1459
+ }
1460
+ return filenameStart;
1461
+ };
1462
+ var isFilePathCandidate = (text, start, end) => {
1463
+ const punctuationEnd = stripTrailingPathPunctuation(text, start, end);
1464
+ const pathEnd = stripLineSuffixes(text, start, punctuationEnd);
1465
+ const dotIndex = findLastDot(text, start, pathEnd);
1466
+ if (dotIndex < 0 || dotIndex + 1 >= pathEnd) return false;
1467
+ if (!matchesKnownExtension(text, dotIndex + 1, pathEnd)) return false;
1468
+ return dotIndex > findFilenameStart(text, start, dotIndex);
1469
+ };
1470
+ var detectFilePaths = (text) => {
1471
+ let candidateStart = -1;
1472
+ let candidateTooLong = false;
1473
+ for (let index = 0; index <= text.length; index++) {
1474
+ const isCandidateChar = index < text.length && isPathCandidateChar(text.charCodeAt(index));
1475
+ if (isCandidateChar) {
1476
+ if (candidateStart < 0) {
1477
+ candidateStart = index;
1478
+ }
1479
+ if (index + 1 - candidateStart > MAX_FILE_PATH_CANDIDATE_LENGTH) {
1480
+ candidateTooLong = true;
1481
+ }
1482
+ continue;
1483
+ }
1484
+ if (candidateStart >= 0) {
1485
+ if (!candidateTooLong && isFilePathCandidate(text, candidateStart, index)) {
1486
+ return true;
1487
+ }
1488
+ candidateStart = -1;
1489
+ candidateTooLong = false;
1490
+ }
1491
+ }
1492
+ return false;
1493
+ };
1494
+ var detectKeywords = (text) => {
1495
+ const result = {};
1496
+ for (const kw of KEYWORDS) {
1497
+ result[kw] = KEYWORD_PATTERNS.get(kw).test(text);
1498
+ }
1499
+ return result;
1500
+ };
1501
+ var extractContextSignals = (observations) => {
1502
+ let hasProjectProfile = false;
1503
+ let searchResultCount = 0;
1504
+ let discoveredFileCount = 0;
1505
+ for (const obs of observations) {
1506
+ switch (obs.type) {
1507
+ case "fs.fileRead":
1508
+ discoveredFileCount++;
1509
+ if (obs.path.endsWith("package.json")) {
1510
+ hasProjectProfile = true;
1511
+ }
1512
+ break;
1513
+ case "fs.searchResult":
1514
+ searchResultCount += obs.matches.length;
1515
+ break;
1516
+ }
1517
+ }
1518
+ return {
1519
+ hasProjectProfile,
1520
+ searchResultCount,
1521
+ discoveredFileCount
1522
+ };
1523
+ };
1524
+ var extractSignals = (state) => {
1525
+ const goalText = state.goal.text;
1526
+ return {
1527
+ goalLengthCategory: categorizeGoalLength(goalText),
1528
+ hasFilePaths: detectFilePaths(goalText),
1529
+ keywords: detectKeywords(goalText),
1530
+ contextSignals: extractContextSignals(state.observations)
1531
+ };
1532
+ };
1533
+
1534
+ // src/agent/core/patchStrategy/promptStrategy.ts
1535
+ var strategyPromptFragment = (strategy) => {
1536
+ switch (strategy) {
1537
+ case "direct-patch":
1538
+ return "Produce a single focused patch in one response. Do not plan multiple steps. Emit one unified diff that addresses the goal directly.";
1539
+ case "multi-step-patch":
1540
+ return "You may produce multiple incremental patches across responses. Start with the most critical change, then refine iteratively based on validation feedback.";
1541
+ case "propose-then-refine":
1542
+ return "First propose a plan describing what changes are needed and why. Do NOT include a patch in this response. After validation feedback, produce the refined patch in a follow-up.";
1543
+ }
1544
+ };
1545
+
1021
1546
  // src/agent/core/decide.ts
1022
1547
  var hasObservation = (state, type) => state.observations.some((obs) => obs.type === type);
1023
1548
  var lastObservation = (state, type) => [...state.observations].reverse().find((obs) => obs.type === type);
@@ -1108,7 +1633,7 @@ var causeMessage = (cause) => {
1108
1633
  return String(cause);
1109
1634
  };
1110
1635
  var errorDetail = (state, cause) => redactForPrompt(state, causeMessage(cause)).slice(0, 2e3);
1111
- var buildPlanningPrompt = (state) => {
1636
+ var buildPlanningPrompt = (state, strategy) => {
1112
1637
  const discovery = discoverValidationCommands(state);
1113
1638
  return redactForPrompt(state, [
1114
1639
  "You are a coding agent running on brass-runtime.",
@@ -1119,6 +1644,7 @@ var buildPlanningPrompt = (state) => {
1119
1644
  "Only propose a patch when the observations are strong enough.",
1120
1645
  "Use the project command discovery summary as context, but do not invent commands that were not run.",
1121
1646
  describeLanguagePolicy(state.goal),
1647
+ strategy ? strategyPromptFragment(strategy) : "",
1122
1648
  "",
1123
1649
  `Goal: ${state.goal.text}`,
1124
1650
  `Workspace: ${state.goal.cwd}`,
@@ -1354,6 +1880,9 @@ var decideNextAction = (state) => {
1354
1880
  const latest = state.observations.at(-1);
1355
1881
  if (latest?.type === "agent.error") {
1356
1882
  if (shouldRequestRepairAfterPatchError(state)) {
1883
+ if (state.goal.llmAvailable === false) {
1884
+ return asyncSucceed({ type: "agent.finish", summary: buildErrorSummary(state) });
1885
+ }
1357
1886
  return asyncSucceed(repairAction(state, "previous patch failed to apply"));
1358
1887
  }
1359
1888
  return asyncSucceed({ type: "agent.finish", summary: buildErrorSummary(state) });
@@ -1389,12 +1918,25 @@ var decideNextAction = (state) => {
1389
1918
  if (!planResponse) {
1390
1919
  const validationAction = nextValidationActionBeforePlanning(state);
1391
1920
  if (validationAction) return asyncSucceed(validationAction);
1392
- const contextAction = nextContextDiscoveryAction(state);
1921
+ const contextAction = nextContextDiscoveryAction(state, state.goal.banditState);
1393
1922
  if (contextAction) return asyncSucceed(contextAction);
1923
+ if (state.goal.llmAvailable === false) {
1924
+ return asyncSucceed({
1925
+ type: "agent.finish",
1926
+ summary: "No LLM provider configured. Tool-only execution complete."
1927
+ });
1928
+ }
1929
+ const signals = extractSignals(state);
1930
+ const strategy = selectStrategy(
1931
+ signals,
1932
+ state.goal.patchStrategy,
1933
+ state.goal.rewardHistory ?? [],
1934
+ { sampleBeta: (a, b) => sampleBeta(a, b, Math.random), random: Math.random }
1935
+ );
1394
1936
  return asyncSucceed({
1395
1937
  type: "llm.complete",
1396
1938
  purpose: "plan",
1397
- prompt: buildPlanningPrompt(state)
1939
+ prompt: buildPlanningPrompt(state, strategy)
1398
1940
  });
1399
1941
  }
1400
1942
  if (isWritableMode(state.goal.mode)) {
@@ -1413,6 +1955,9 @@ var decideNextAction = (state) => {
1413
1955
  const validationAction = nextValidationActionAfterPatch(state);
1414
1956
  if (validationAction) return asyncSucceed(validationAction);
1415
1957
  if (shouldRequestRepairAfterValidation(state)) {
1958
+ if (state.goal.llmAvailable === false) {
1959
+ return asyncSucceed({ type: "agent.finish", summary: buildErrorSummary(state) });
1960
+ }
1416
1961
  return asyncSucceed(repairAction(state, "validation failed after applying the generated patch"));
1417
1962
  }
1418
1963
  if (shouldAutoRollbackAfterFinalValidationFailure(state)) {
@@ -1632,13 +2177,22 @@ var actionToEffect = (action, state) => {
1632
2177
  )
1633
2178
  );
1634
2179
  case "llm.complete":
1635
- return asyncFlatMap(
1636
- service("llm"),
1637
- (llm) => asyncMap(
2180
+ return asyncFlatMap(service("llm"), (llm) => {
2181
+ if (!llm) {
2182
+ return asyncFail({
2183
+ _tag: "LLMError",
2184
+ cause: "llm_unavailable: no LLM provider is configured"
2185
+ });
2186
+ }
2187
+ return asyncMap(
1638
2188
  llm.complete({ purpose: action.purpose, prompt: action.prompt }),
1639
- (response) => ({ type: "llm.response", purpose: action.purpose, content: response.content })
1640
- )
1641
- );
2189
+ (response) => ({
2190
+ type: "llm.response",
2191
+ purpose: action.purpose,
2192
+ content: response.content
2193
+ })
2194
+ );
2195
+ });
1642
2196
  case "patch.propose":
1643
2197
  return asyncSucceed({ type: "patch.proposed", patch: action.patch });
1644
2198
  case "patch.apply":
@@ -1821,7 +2375,265 @@ var invokeAction = (action, state, scope) => asyncFlatMap(
1821
2375
  })
1822
2376
  );
1823
2377
 
2378
+ // src/agent/core/llmBudget/config.ts
2379
+ var DEFAULTS = {
2380
+ overshootFraction: 0.1,
2381
+ enabled: true
2382
+ };
2383
+ var resolveBudgetConfig = (goalBudget, configBudget) => {
2384
+ if (goalBudget === void 0 && configBudget === void 0) {
2385
+ return void 0;
2386
+ }
2387
+ const merged = {
2388
+ ...configBudget,
2389
+ ...goalBudget
2390
+ };
2391
+ if (merged.tokenBudget === void 0) {
2392
+ return void 0;
2393
+ }
2394
+ return {
2395
+ tokenBudget: merged.tokenBudget,
2396
+ overshootFraction: merged.overshootFraction ?? DEFAULTS.overshootFraction,
2397
+ enabled: merged.enabled ?? DEFAULTS.enabled,
2398
+ modelTiers: merged.modelTiers
2399
+ };
2400
+ };
2401
+ var validateBudgetConfig = (config) => {
2402
+ if (!Number.isFinite(config.tokenBudget) || config.tokenBudget <= 0) {
2403
+ return `tokenBudget must be a positive finite number, got ${config.tokenBudget}`;
2404
+ }
2405
+ if (!Number.isFinite(config.overshootFraction) || config.overshootFraction < 0 || config.overshootFraction > 1) {
2406
+ return `overshootFraction must be between 0 and 1 inclusive, got ${config.overshootFraction}`;
2407
+ }
2408
+ return void 0;
2409
+ };
2410
+
2411
+ // src/agent/core/llmBudget/state.ts
2412
+ var initBudgetState = () => ({
2413
+ totalInputTokens: 0,
2414
+ totalOutputTokens: 0,
2415
+ totalTokens: 0,
2416
+ callCount: 0,
2417
+ calls: []
2418
+ });
2419
+ var updateBudgetState = (state, usage, tier, confidence, estimated) => {
2420
+ const totalInputTokens = state.totalInputTokens + usage.inputTokens;
2421
+ const totalOutputTokens = state.totalOutputTokens + usage.outputTokens;
2422
+ return {
2423
+ totalInputTokens,
2424
+ totalOutputTokens,
2425
+ totalTokens: totalInputTokens + totalOutputTokens,
2426
+ callCount: state.callCount + 1,
2427
+ calls: [
2428
+ ...state.calls,
2429
+ { usage, tier, confidence, estimated }
2430
+ ]
2431
+ };
2432
+ };
2433
+ var budgetStatus = (state, config) => {
2434
+ const { totalTokens } = state;
2435
+ const { tokenBudget, overshootFraction } = config;
2436
+ const hardCap = tokenBudget * (1 + overshootFraction);
2437
+ if (totalTokens <= tokenBudget) {
2438
+ return { type: "under" };
2439
+ }
2440
+ if (totalTokens <= hardCap) {
2441
+ return { type: "warning", overage: totalTokens - tokenBudget };
2442
+ }
2443
+ return { type: "exceeded", overage: totalTokens - tokenBudget };
2444
+ };
2445
+ var budgetAllowsCall = (state, config) => {
2446
+ return budgetStatus(state, config).type !== "exceeded";
2447
+ };
2448
+
2449
+ // src/agent/core/llmBudget/estimation.ts
2450
+ var estimateTokens = (promptLength, responseLength) => ({
2451
+ inputTokens: Math.ceil(promptLength / 4),
2452
+ outputTokens: Math.ceil(responseLength / 4)
2453
+ });
2454
+
2455
+ // src/agent/core/llmBudget/confidence.ts
2456
+ var HEDGING_PHRASES = [
2457
+ "i think",
2458
+ "maybe",
2459
+ "perhaps",
2460
+ "not sure",
2461
+ "might be",
2462
+ "could be"
2463
+ ];
2464
+ var hasDiffBlock = (response) => {
2465
+ if (response.includes("```diff")) return true;
2466
+ const lines = response.split("\n");
2467
+ return lines.some(
2468
+ (line) => line.startsWith("---") || line.startsWith("+++")
2469
+ );
2470
+ };
2471
+ var isConcise = (response) => response.length < 2e3;
2472
+ var referencesGoal = (response, goal) => {
2473
+ const words = goal.split(/\s+/).filter((w) => w.length >= 3).map((w) => w.toLowerCase());
2474
+ const responseLower = response.toLowerCase();
2475
+ return words.some((word) => responseLower.includes(word));
2476
+ };
2477
+ var referencesReadFiles = (response, readFiles) => {
2478
+ if (readFiles.length === 0) return false;
2479
+ return readFiles.some((filePath) => response.includes(filePath));
2480
+ };
2481
+ var countHedgingPhrases = (response) => {
2482
+ const responseLower = response.toLowerCase();
2483
+ let count = 0;
2484
+ for (const phrase of HEDGING_PHRASES) {
2485
+ let idx = 0;
2486
+ while (true) {
2487
+ const found = responseLower.indexOf(phrase, idx);
2488
+ if (found === -1) break;
2489
+ count++;
2490
+ idx = found + phrase.length;
2491
+ }
2492
+ }
2493
+ return count;
2494
+ };
2495
+ var extractConfidenceSignals = (response, goal, readFiles) => ({
2496
+ hasDiffBlock: hasDiffBlock(response),
2497
+ isConcise: isConcise(response),
2498
+ referencesGoal: referencesGoal(response, goal),
2499
+ referencesReadFiles: referencesReadFiles(response, readFiles),
2500
+ hedgingCount: countHedgingPhrases(response)
2501
+ });
2502
+ var estimateConfidence = (response, goal, readFiles) => {
2503
+ const signals = extractConfidenceSignals(response, goal, readFiles);
2504
+ let score = 0.35;
2505
+ if (signals.hasDiffBlock) score += 0.2;
2506
+ if (signals.isConcise) score += 0.15;
2507
+ if (signals.referencesGoal) score += 0.15;
2508
+ if (signals.referencesReadFiles) score += 0.15;
2509
+ const hedgingPenalty = Math.min(signals.hedgingCount * 0.1, 0.3);
2510
+ score -= hedgingPenalty;
2511
+ score = Math.max(0, Math.min(1, score));
2512
+ return { score, signals };
2513
+ };
2514
+
2515
+ // src/agent/core/llmBudget/router.ts
2516
+ var DEFAULT_THRESHOLDS = {
2517
+ goalLength: 500,
2518
+ filesRead: 5,
2519
+ searchMatches: 30,
2520
+ repairAttempts: 1
2521
+ };
2522
+ var extractComplexitySignals = (state) => {
2523
+ const observations = state.observations;
2524
+ const goalLength = state.goal.text.length;
2525
+ let filesRead = 0;
2526
+ let searchMatches2 = 0;
2527
+ let hasValidationErrors = false;
2528
+ let repairAttempts = 0;
2529
+ for (const obs of observations) {
2530
+ switch (obs.type) {
2531
+ case "fs.fileRead":
2532
+ filesRead++;
2533
+ break;
2534
+ case "fs.searchResult":
2535
+ searchMatches2 += obs.matches.length;
2536
+ break;
2537
+ case "shell.result":
2538
+ if (obs.exitCode !== 0) {
2539
+ hasValidationErrors = true;
2540
+ }
2541
+ break;
2542
+ case "llm.response":
2543
+ if (obs.purpose === "patch") {
2544
+ repairAttempts++;
2545
+ }
2546
+ break;
2547
+ }
2548
+ }
2549
+ return {
2550
+ goalLength,
2551
+ filesRead,
2552
+ searchMatches: searchMatches2,
2553
+ hasValidationErrors,
2554
+ repairAttempts
2555
+ };
2556
+ };
2557
+ var routeModel = (state, _budgetState, thresholds) => {
2558
+ const t = thresholds ?? DEFAULT_THRESHOLDS;
2559
+ const signals = extractComplexitySignals(state);
2560
+ if (signals.goalLength >= t.goalLength || signals.filesRead >= t.filesRead || signals.searchMatches >= t.searchMatches || signals.hasValidationErrors || signals.repairAttempts >= t.repairAttempts) {
2561
+ return "large";
2562
+ }
2563
+ return "small";
2564
+ };
2565
+
2566
+ // src/agent/core/llmBudget/events.ts
2567
+ var makeBudgetUsageEvent = (usage, cumulative, tier, remaining) => ({
2568
+ type: "budget.usage",
2569
+ usage,
2570
+ cumulative,
2571
+ tier,
2572
+ remaining,
2573
+ at: Date.now()
2574
+ });
2575
+ var makeBudgetRoutedEvent = (tier, signals, resolvedProvider) => ({
2576
+ type: "budget.routed",
2577
+ tier,
2578
+ signals,
2579
+ resolvedProvider,
2580
+ at: Date.now()
2581
+ });
2582
+ var makeBudgetConfidenceEvent = (score, signals, purpose) => ({
2583
+ type: "budget.confidence",
2584
+ score,
2585
+ signals,
2586
+ purpose,
2587
+ at: Date.now()
2588
+ });
2589
+ var makeBudgetWarningEvent = (totalTokens, tokenBudget) => ({
2590
+ type: "budget.warning",
2591
+ totalTokens,
2592
+ tokenBudget,
2593
+ at: Date.now()
2594
+ });
2595
+ var makeBudgetExceededEvent = (totalTokens, tokenBudget, overshootFraction, hardCap) => ({
2596
+ type: "budget.exceeded",
2597
+ totalTokens,
2598
+ tokenBudget,
2599
+ overshootFraction,
2600
+ hardCap,
2601
+ at: Date.now()
2602
+ });
2603
+
2604
+ // src/agent/core/llmBudget/persistence.ts
2605
+ var isValidRecord = (r) => {
2606
+ if (r === null || typeof r !== "object") return false;
2607
+ const rec = r;
2608
+ return typeof rec.goalId === "string" && typeof rec.totalTokens === "number" && typeof rec.callCount === "number" && (rec.tier === "small" || rec.tier === "large") && typeof rec.confidence === "number" && typeof rec.timestamp === "number";
2609
+ };
2610
+ var parseLearningStore = (json) => {
2611
+ try {
2612
+ const parsed = JSON.parse(json);
2613
+ if (parsed === null || typeof parsed !== "object") {
2614
+ return { records: [] };
2615
+ }
2616
+ if (!Array.isArray(parsed.records)) {
2617
+ return { records: [] };
2618
+ }
2619
+ const validRecords = parsed.records.filter(isValidRecord);
2620
+ return { records: validRecords };
2621
+ } catch {
2622
+ return { records: [] };
2623
+ }
2624
+ };
2625
+ var appendRunRecord = (store, record, maxRecords = 100) => {
2626
+ const updated = [...store.records, record];
2627
+ if (updated.length > maxRecords) {
2628
+ return { records: updated.slice(updated.length - maxRecords) };
2629
+ }
2630
+ return { records: updated };
2631
+ };
2632
+ var serializeLearningStore = (store) => JSON.stringify(store, null, 2);
2633
+
1824
2634
  // src/agent/core/runAgent.ts
2635
+ import { readFile, writeFile, mkdir } from "fs/promises";
2636
+ import { dirname } from "path";
1825
2637
  var executeAction = (action, state, scope) => asyncFlatMap(
1826
2638
  nowMillis(),
1827
2639
  (startedAt) => asyncFlatMap(
@@ -1885,45 +2697,197 @@ var recordObservation = (next, observation) => asyncFlatMap(nowMillis(), (at) =>
1885
2697
  ];
1886
2698
  return emitAgentEvents(events);
1887
2699
  });
1888
- var runLoop = (state, scope, runStartedAt) => {
2700
+ var buildBudgetExhaustedSummary = (state) => {
2701
+ const observations = state.observations;
2702
+ const fileReads = observations.filter((o) => o.type === "fs.fileRead").length;
2703
+ const llmCalls = observations.filter((o) => o.type === "llm.response").length;
2704
+ if (state.phase === "planning") {
2705
+ const hasPlan = observations.some((o) => o.type === "llm.response");
2706
+ if (!hasPlan) {
2707
+ return "Budget exhausted before planning could complete. No plan was generated within the token budget.";
2708
+ }
2709
+ }
2710
+ if (state.phase === "validating") {
2711
+ return `Budget exhausted during validation. Completed ${state.steps} steps (${fileReads} file reads, ${llmCalls} LLM calls). In-progress non-LLM validation commands were allowed to complete.`;
2712
+ }
2713
+ return `Budget exhausted. Completed ${state.steps} steps (${fileReads} file reads, ${llmCalls} LLM calls) before reaching the token budget hard cap.`;
2714
+ };
2715
+ var extractReadFiles = (state) => state.observations.filter((o) => o.type === "fs.fileRead").map((o) => o.path);
2716
+ var LEARNING_STORE_PATH = ".brass/llm-budget.json";
2717
+ var persistLearningRecord = (state, budgetState) => asyncFold(
2718
+ asyncInterruptible((_env, cb) => {
2719
+ const filePath = `${state.goal.cwd}/${LEARNING_STORE_PATH}`;
2720
+ const run = async () => {
2721
+ const lastCall = budgetState.calls[budgetState.calls.length - 1];
2722
+ const tier = lastCall?.tier ?? "small";
2723
+ const confidence = budgetState.callCount > 0 ? budgetState.calls.reduce((sum, c) => sum + c.confidence, 0) / budgetState.callCount : 0;
2724
+ const record = {
2725
+ goalId: state.goal.id,
2726
+ totalTokens: budgetState.totalTokens,
2727
+ callCount: budgetState.callCount,
2728
+ tier,
2729
+ confidence,
2730
+ timestamp: Date.now()
2731
+ };
2732
+ let existingJson = "";
2733
+ try {
2734
+ existingJson = await readFile(filePath, "utf8");
2735
+ } catch {
2736
+ }
2737
+ const store = parseLearningStore(existingJson);
2738
+ const updated = appendRunRecord(store, record);
2739
+ const serialized = serializeLearningStore(updated);
2740
+ await mkdir(dirname(filePath), { recursive: true });
2741
+ await writeFile(filePath, serialized, "utf8");
2742
+ };
2743
+ run().then(
2744
+ () => cb({ _tag: "Success", value: void 0 }),
2745
+ (err) => cb({ _tag: "Failure", cause: { _tag: "Fail", error: { _tag: "FsError", operation: "persistLearningStore", cause: err } } })
2746
+ );
2747
+ }),
2748
+ // On failure: swallow the error silently (Requirement 7.4)
2749
+ () => asyncSucceed(void 0),
2750
+ // On success: pass through
2751
+ () => asyncSucceed(void 0)
2752
+ );
2753
+ var executeBudgetGatedLLMCall = (action, state, budgetState, budgetConfig, scope) => {
2754
+ if (!budgetAllowsCall(budgetState, budgetConfig)) {
2755
+ const hardCap = budgetConfig.tokenBudget * (1 + budgetConfig.overshootFraction);
2756
+ const exceededEvent = makeBudgetExceededEvent(
2757
+ budgetState.totalTokens,
2758
+ budgetConfig.tokenBudget,
2759
+ budgetConfig.overshootFraction,
2760
+ hardCap
2761
+ );
2762
+ return asyncFlatMap(emitAgentEvent(exceededEvent), () => {
2763
+ const finishObservation = {
2764
+ type: "agent.done",
2765
+ summary: buildBudgetExhaustedSummary(state)
2766
+ };
2767
+ return asyncSucceed({ observation: finishObservation, budgetState });
2768
+ });
2769
+ }
2770
+ const tier = routeModel(state, budgetState);
2771
+ const signals = extractComplexitySignals(state);
2772
+ const resolvedProvider = budgetConfig.modelTiers?.[tier]?.provider;
2773
+ const routedEvent = makeBudgetRoutedEvent(tier, signals, resolvedProvider);
2774
+ return asyncFlatMap(
2775
+ emitAgentEvent(routedEvent),
2776
+ () => (
2777
+ // Execute the LLM call through the normal action execution path
2778
+ // We use the LLM service directly to capture the full LLMResponse with usage
2779
+ asyncFlatMap(asyncSync((env) => env.llm), (llm) => {
2780
+ if (!llm) {
2781
+ return asyncFail({
2782
+ _tag: "LLMError",
2783
+ cause: "llm_unavailable: no LLM provider is configured"
2784
+ });
2785
+ }
2786
+ return asyncFlatMap(
2787
+ llm.complete({ purpose: action.purpose, prompt: action.prompt }),
2788
+ (response) => {
2789
+ const usage = response.usage ?? estimateTokens(
2790
+ action.prompt.length,
2791
+ response.content.length
2792
+ );
2793
+ const estimated = response.usage === void 0;
2794
+ const readFiles = extractReadFiles(state);
2795
+ const { score: confidence, signals: confidenceSignals } = estimateConfidence(
2796
+ response.content,
2797
+ state.goal.text,
2798
+ readFiles
2799
+ );
2800
+ const newBudgetState = updateBudgetState(budgetState, usage, tier, confidence, estimated);
2801
+ const remaining = Math.max(0, budgetConfig.tokenBudget - newBudgetState.totalTokens);
2802
+ const usageEvent = makeBudgetUsageEvent(
2803
+ usage,
2804
+ { totalTokens: newBudgetState.totalTokens, callCount: newBudgetState.callCount },
2805
+ tier,
2806
+ remaining
2807
+ );
2808
+ const confidenceEvent = makeBudgetConfidenceEvent(confidence, confidenceSignals, action.purpose);
2809
+ const events = [usageEvent, confidenceEvent];
2810
+ if (newBudgetState.totalTokens > budgetConfig.tokenBudget) {
2811
+ events.push(makeBudgetWarningEvent(newBudgetState.totalTokens, budgetConfig.tokenBudget));
2812
+ }
2813
+ return asyncFlatMap(emitAgentEvents(events), () => {
2814
+ const observation = {
2815
+ type: "llm.response",
2816
+ purpose: action.purpose,
2817
+ content: response.content
2818
+ };
2819
+ return asyncSucceed({ observation, budgetState: newBudgetState });
2820
+ });
2821
+ }
2822
+ );
2823
+ })
2824
+ )
2825
+ );
2826
+ };
2827
+ var runLoop = (state, budgetState, budgetConfig, scope, runStartedAt) => {
1889
2828
  if (isTerminal(state)) {
2829
+ const persistEffect = budgetConfig !== void 0 && budgetState !== void 0 ? persistLearningRecord(state, budgetState) : asyncSucceed(void 0);
1890
2830
  return asyncFlatMap(
1891
- nowMillis(),
1892
- (at) => asyncFlatMap(
1893
- emitAgentEvent({
1894
- type: "agent.run.completed",
1895
- goal: state.goal,
1896
- status: runStatusFor(state.phase),
1897
- phase: state.phase,
1898
- steps: state.steps,
1899
- durationMs: at - runStartedAt,
1900
- at
1901
- }),
1902
- () => asyncSucceed(state)
2831
+ persistEffect,
2832
+ () => asyncFlatMap(
2833
+ nowMillis(),
2834
+ (at) => asyncFlatMap(
2835
+ emitAgentEvent({
2836
+ type: "agent.run.completed",
2837
+ goal: state.goal,
2838
+ status: runStatusFor(state.phase),
2839
+ phase: state.phase,
2840
+ steps: state.steps,
2841
+ durationMs: at - runStartedAt,
2842
+ at
2843
+ }),
2844
+ () => asyncSucceed(state)
2845
+ )
1903
2846
  )
1904
2847
  );
1905
2848
  }
1906
- return asyncFlatMap(
1907
- decideNextAction(state),
1908
- (action) => asyncFlatMap(executeAction(action, state, scope), (observation) => {
2849
+ return asyncFlatMap(decideNextAction(state), (action) => {
2850
+ if (action.type === "llm.complete" && budgetConfig !== void 0 && budgetState !== void 0) {
2851
+ return asyncFlatMap(
2852
+ executeBudgetGatedLLMCall(action, state, budgetState, budgetConfig, scope),
2853
+ (result) => {
2854
+ const next = reduceAgentState(state, result.observation);
2855
+ return asyncFlatMap(
2856
+ recordObservation(next, result.observation),
2857
+ () => runLoop(next, result.budgetState, budgetConfig, scope, runStartedAt)
2858
+ );
2859
+ }
2860
+ );
2861
+ }
2862
+ return asyncFlatMap(executeAction(action, state, scope), (observation) => {
1909
2863
  const next = reduceAgentState(state, observation);
1910
2864
  return asyncFlatMap(
1911
2865
  recordObservation(next, observation),
1912
- () => runLoop(next, scope, runStartedAt)
2866
+ () => runLoop(next, budgetState, budgetConfig, scope, runStartedAt)
1913
2867
  );
1914
- })
1915
- );
2868
+ });
2869
+ });
1916
2870
  };
1917
- var runAgent = (runtime, goal) => withScopeAsync(
1918
- runtime,
1919
- (scope) => asyncFlatMap(
1920
- nowMillis(),
1921
- (startedAt) => asyncFlatMap(
1922
- emitAgentEvent({ type: "agent.run.started", goal, at: startedAt }),
1923
- () => runLoop(initialAgentState(goal), scope, startedAt)
2871
+ var runAgent = (runtime, goal, configBudget) => {
2872
+ const budgetConfig = resolveBudgetConfig(goal.budget, configBudget);
2873
+ if (budgetConfig !== void 0) {
2874
+ const validationError = validateBudgetConfig(budgetConfig);
2875
+ if (validationError !== void 0) {
2876
+ return asyncFail({ _tag: "AgentLoopError", message: validationError });
2877
+ }
2878
+ }
2879
+ const budgetState = budgetConfig !== void 0 ? initBudgetState() : void 0;
2880
+ return withScopeAsync(
2881
+ runtime,
2882
+ (scope) => asyncFlatMap(
2883
+ nowMillis(),
2884
+ (startedAt) => asyncFlatMap(
2885
+ emitAgentEvent({ type: "agent.run.started", goal, at: startedAt }),
2886
+ () => runLoop(initialAgentState(goal), budgetState, budgetConfig, scope, startedAt)
2887
+ )
1924
2888
  )
1925
- )
1926
- );
2889
+ );
2890
+ };
1927
2891
 
1928
2892
  // src/agent/core/config.ts
1929
2893
  var isAgentConfigMode = (value) => value === "read-only" || value === "propose" || value === "write" || value === "autonomous";
@@ -1946,6 +2910,243 @@ var goalForAgentPreset = (preset) => {
1946
2910
  }
1947
2911
  };
1948
2912
 
2913
+ // src/agent/core/hostProfile.ts
2914
+ var deepFreeze = (obj) => {
2915
+ Object.freeze(obj);
2916
+ for (const name of Object.getOwnPropertyNames(obj)) {
2917
+ const value = obj[name];
2918
+ if (value !== null && typeof value === "object" && !Object.isFrozen(value)) {
2919
+ deepFreeze(value);
2920
+ }
2921
+ }
2922
+ return obj;
2923
+ };
2924
+
2925
+ // src/agent/core/hostSignals.ts
2926
+ var MAX_ENV_KEYS = 256;
2927
+ var collectHostSignals = (input) => {
2928
+ const signals = [];
2929
+ try {
2930
+ for (const arg of input.argv) {
2931
+ signals.push({ source: "argv", value: arg });
2932
+ }
2933
+ } catch {
2934
+ }
2935
+ try {
2936
+ const keys = Object.keys(input.env).sort();
2937
+ const limit = Math.min(keys.length, MAX_ENV_KEYS);
2938
+ for (let i = 0; i < limit; i++) {
2939
+ signals.push({ source: "env-key", value: keys[i] });
2940
+ }
2941
+ } catch {
2942
+ }
2943
+ try {
2944
+ signals.push({
2945
+ source: "stdio",
2946
+ value: input.stdoutIsTTY ? "stdout:tty" : "stdout:pipe"
2947
+ });
2948
+ signals.push({
2949
+ source: "stdio",
2950
+ value: input.stdinIsTTY ? "stdin:tty" : "stdin:pipe"
2951
+ });
2952
+ if (input.ttyColumns !== void 0) {
2953
+ signals.push({ source: "stdio", value: `columns:${input.ttyColumns}` });
2954
+ }
2955
+ } catch {
2956
+ }
2957
+ try {
2958
+ if (input.parentProcessName !== void 0) {
2959
+ signals.push({ source: "parent-process", value: input.parentProcessName });
2960
+ }
2961
+ } catch {
2962
+ }
2963
+ try {
2964
+ for (const marker of input.workspaceMarkers) {
2965
+ signals.push({ source: "workspace-marker", value: marker });
2966
+ }
2967
+ } catch {
2968
+ }
2969
+ try {
2970
+ if (input.stdinFirstLine !== void 0 && !input.stdinIsTTY) {
2971
+ signals.push({ source: "protocol-handshake", value: input.stdinFirstLine });
2972
+ }
2973
+ } catch {
2974
+ }
2975
+ try {
2976
+ for (const configPath of input.configPaths) {
2977
+ signals.push({ source: "config", value: configPath });
2978
+ }
2979
+ } catch {
2980
+ }
2981
+ return deepFreeze(signals);
2982
+ };
2983
+
2984
+ // src/agent/core/hostInference.ts
2985
+ var CI_INDICATORS = [
2986
+ "CI",
2987
+ "GITHUB_ACTIONS",
2988
+ "JENKINS_URL",
2989
+ "CIRCLECI",
2990
+ "TRAVIS",
2991
+ "GITLAB_CI",
2992
+ "BUILDKITE",
2993
+ "TF_BUILD"
2994
+ ];
2995
+ var inferTransport = (signals) => {
2996
+ const hasProtocolMcp = signals.some(
2997
+ (s) => s.source === "protocol-handshake" && s.value.toLowerCase().includes("mcp")
2998
+ );
2999
+ if (hasProtocolMcp) return "mcp";
3000
+ const extensionMarkers = [".vscode", ".cursor", ".kiro"];
3001
+ const hasExtensionMarker = signals.some(
3002
+ (s) => s.source === "workspace-marker" && extensionMarkers.some((m) => s.value.toLowerCase().includes(m))
3003
+ );
3004
+ const hasExtensionConfig = signals.some(
3005
+ (s) => s.source === "config" && extensionMarkers.some((m) => s.value.toLowerCase().includes(m))
3006
+ );
3007
+ if (hasExtensionMarker || hasExtensionConfig) return "extension";
3008
+ const hasCiEnv = signals.some(
3009
+ (s) => s.source === "env-key" && CI_INDICATORS.some((ci) => s.value === ci)
3010
+ );
3011
+ if (hasCiEnv) return "ci";
3012
+ const stdoutIsTty = signals.some(
3013
+ (s) => s.source === "stdio" && s.value === "stdout:tty"
3014
+ );
3015
+ if (stdoutIsTty) return "terminal";
3016
+ const stdoutIsPipe = signals.some(
3017
+ (s) => s.source === "stdio" && s.value === "stdout:pipe"
3018
+ );
3019
+ if (stdoutIsPipe) return "stdio";
3020
+ return "unknown";
3021
+ };
3022
+ var inferCapabilities = (signals, transport) => {
3023
+ const hasOwnLLM = signals.some(
3024
+ (s) => s.source === "env-key" && (s.value.toUpperCase().includes("LLM") || s.value.toUpperCase().includes("AI_API")) || s.source === "protocol-handshake" && s.value.toLowerCase().includes("llm")
3025
+ );
3026
+ const wantsJson = transport === "stdio" || transport === "mcp";
3027
+ const supportsStreamingEvents = transport === "terminal" || transport === "mcp" || transport === "extension";
3028
+ const supportsMcp = transport === "mcp";
3029
+ const stdinIsTty = signals.some(
3030
+ (s) => s.source === "stdio" && s.value === "stdin:tty"
3031
+ );
3032
+ const canAskApproval = stdinIsTty || transport === "extension" || transport === "mcp";
3033
+ const ttyColumnsSignal = signals.find(
3034
+ (s) => s.source === "stdio" && s.value.startsWith("columns:")
3035
+ );
3036
+ const ttyColumns = ttyColumnsSignal ? parseInt(ttyColumnsSignal.value.slice("columns:".length), 10) : void 0;
3037
+ const stdoutIsTty = signals.some(
3038
+ (s) => s.source === "stdio" && s.value === "stdout:tty"
3039
+ );
3040
+ const canRenderDiff = stdoutIsTty && ttyColumns !== void 0 && ttyColumns >= 80 || transport === "extension";
3041
+ const canApplyPatch = transport === "extension" || transport === "mcp";
3042
+ const interactiveTty = stdoutIsTty;
3043
+ return {
3044
+ hasOwnLLM,
3045
+ wantsJson,
3046
+ supportsStreamingEvents,
3047
+ supportsMcp,
3048
+ canAskApproval,
3049
+ canRenderDiff,
3050
+ canApplyPatch,
3051
+ interactiveTty
3052
+ };
3053
+ };
3054
+ var inferConstraints = (capabilities, transport) => {
3055
+ const readOnlyByDefault = transport === "ci";
3056
+ const patchPreviewRequired = capabilities.canRenderDiff === true && capabilities.canApplyPatch === false;
3057
+ const requireNoNetwork = false;
3058
+ return {
3059
+ readOnlyByDefault,
3060
+ patchPreviewRequired,
3061
+ requireNoNetwork
3062
+ };
3063
+ };
3064
+ var IDENTITY_PATTERNS = [
3065
+ {
3066
+ name: "cursor",
3067
+ match: (signals) => {
3068
+ if (signals.some((s) => s.source === "workspace-marker" && s.value.toLowerCase().includes(".cursor"))) {
3069
+ return 0.8;
3070
+ }
3071
+ return 0;
3072
+ }
3073
+ },
3074
+ {
3075
+ name: "vscode",
3076
+ match: (signals) => {
3077
+ if (signals.some((s) => s.source === "workspace-marker" && s.value.toLowerCase().includes(".vscode"))) {
3078
+ return 0.8;
3079
+ }
3080
+ return 0;
3081
+ }
3082
+ },
3083
+ {
3084
+ name: "kiro",
3085
+ match: (signals) => {
3086
+ if (signals.some((s) => s.source === "workspace-marker" && s.value.toLowerCase().includes(".kiro"))) {
3087
+ return 0.8;
3088
+ }
3089
+ if (signals.some((s) => s.source === "env-key" && s.value.startsWith("KIRO_"))) {
3090
+ return 0.7;
3091
+ }
3092
+ return 0;
3093
+ }
3094
+ },
3095
+ {
3096
+ name: "codex",
3097
+ match: (signals) => {
3098
+ if (signals.some((s) => s.source === "parent-process" && s.value.toLowerCase().includes("codex"))) {
3099
+ return 0.9;
3100
+ }
3101
+ if (signals.some((s) => s.source === "env-key" && s.value.startsWith("CODEX_"))) {
3102
+ return 0.7;
3103
+ }
3104
+ return 0;
3105
+ }
3106
+ },
3107
+ {
3108
+ name: "claude-code",
3109
+ match: (signals) => {
3110
+ if (signals.some((s) => s.source === "parent-process" && s.value.toLowerCase().includes("claude"))) {
3111
+ return 0.9;
3112
+ }
3113
+ return 0;
3114
+ }
3115
+ }
3116
+ ];
3117
+ var inferOptionalIdentity = (signals) => {
3118
+ let bestName;
3119
+ let bestConfidence = 0;
3120
+ for (const pattern of IDENTITY_PATTERNS) {
3121
+ const confidence = pattern.match(signals);
3122
+ if (confidence > bestConfidence) {
3123
+ bestConfidence = confidence;
3124
+ bestName = pattern.name;
3125
+ }
3126
+ }
3127
+ if (bestName === void 0 || bestConfidence <= 0) {
3128
+ return void 0;
3129
+ }
3130
+ return {
3131
+ name: bestName,
3132
+ confidence: Math.round(bestConfidence * 100) / 100
3133
+ };
3134
+ };
3135
+ var buildHostProfile = (input) => {
3136
+ const signals = collectHostSignals(input);
3137
+ const transport = inferTransport(signals);
3138
+ const capabilities = inferCapabilities(signals, transport);
3139
+ const constraints = inferConstraints(capabilities, transport);
3140
+ const identity = inferOptionalIdentity(signals);
3141
+ return deepFreeze({
3142
+ transport,
3143
+ capabilities,
3144
+ constraints,
3145
+ identity,
3146
+ evidence: signals
3147
+ });
3148
+ };
3149
+
1949
3150
  // src/agent/tools/permissions.ts
1950
3151
  var DEFAULT_SAFE_SHELL_PATTERNS = [
1951
3152
  "npm test",
@@ -2221,8 +3422,8 @@ var parseRipgrep = (stdout) => stdout.split("\n").filter(Boolean).map((line) =>
2221
3422
  var makeNodeFileSystem = (shell) => ({
2222
3423
  readFile: (path) => fromPromiseAbortable(
2223
3424
  async (signal) => {
2224
- const { readFile } = await dynamicImport2("node:fs/promises");
2225
- return readFile(path, { encoding: "utf8", signal });
3425
+ const { readFile: readFile2 } = await dynamicImport2("node:fs/promises");
3426
+ return readFile2(path, { encoding: "utf8", signal });
2226
3427
  },
2227
3428
  (cause) => ({ _tag: "FsError", operation: "readFile", cause })
2228
3429
  ),
@@ -2556,6 +3757,9 @@ var validateAgentConfig = (config, sourcePath) => {
2556
3757
  config.batch.goals.forEach((goal, index) => validateBatchGoal(goal, `config.batch.goals[${index}]`));
2557
3758
  }
2558
3759
  }
3760
+ if (config.budget !== void 0) {
3761
+ if (!isRecord3(config.budget)) throw new Error("config.budget must be an object.");
3762
+ }
2559
3763
  return config;
2560
3764
  };
2561
3765
  var isFile = async (path) => {
@@ -2580,8 +3784,8 @@ var findConfigPath = async (cwd) => {
2580
3784
  }
2581
3785
  };
2582
3786
  var readConfigFile = async (path) => {
2583
- const { readFile } = await dynamicImport3("node:fs/promises");
2584
- const raw = String(await readFile(path, "utf8")).replace(/^\uFEFF/, "");
3787
+ const { readFile: readFile2 } = await dynamicImport3("node:fs/promises");
3788
+ const raw = String(await readFile2(path, "utf8")).replace(/^\uFEFF/, "");
2585
3789
  try {
2586
3790
  return validateAgentConfig(JSON.parse(raw), path);
2587
3791
  } catch (error) {
@@ -2737,7 +3941,7 @@ var makeFakeLLM = (options = {}) => ({
2737
3941
 
2738
3942
  // src/agent/node/nodeWorkspaceDiscovery.ts
2739
3943
  import { existsSync, statSync } from "fs";
2740
- import { dirname, join, resolve } from "path";
3944
+ import { dirname as dirname2, join, resolve } from "path";
2741
3945
  var WORKSPACE_MARKERS = [
2742
3946
  { name: ".brass-agent.json", kind: "file" },
2743
3947
  { name: "brass-agent.config.json", kind: "file" },
@@ -2793,7 +3997,7 @@ var discoverNodeWorkspaceRoot = (cwd, options = {}) => {
2793
3997
  changed: current !== inputCwd
2794
3998
  };
2795
3999
  }
2796
- const parent = dirname(current);
4000
+ const parent = dirname2(current);
2797
4001
  if (parent === current) {
2798
4002
  return {
2799
4003
  inputCwd,
@@ -2809,6 +4013,7 @@ export {
2809
4013
  initialAgentState,
2810
4014
  reduceAgentState,
2811
4015
  isTerminal,
4016
+ sampleBeta,
2812
4017
  extractLikelyFilePaths,
2813
4018
  deriveContextSearchQueries,
2814
4019
  describeContextDiscovery,
@@ -2847,6 +4052,8 @@ export {
2847
4052
  responseLanguageName,
2848
4053
  describeLanguagePolicy,
2849
4054
  spanishLike,
4055
+ selectStrategy,
4056
+ extractSignals,
2850
4057
  decideNextAction,
2851
4058
  nowMillis,
2852
4059
  emitAgentEvent,
@@ -2868,6 +4075,14 @@ export {
2868
4075
  AGENT_CONFIG_FILE_NAMES,
2869
4076
  isAgentPreset,
2870
4077
  goalForAgentPreset,
4078
+ deepFreeze,
4079
+ collectHostSignals,
4080
+ CI_INDICATORS,
4081
+ inferTransport,
4082
+ inferCapabilities,
4083
+ inferConstraints,
4084
+ inferOptionalIdentity,
4085
+ buildHostProfile,
2871
4086
  makeConfiguredPermissions,
2872
4087
  defaultPermissions,
2873
4088
  autoApproveApprovals,