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.
- package/CHANGELOG.md +54 -0
- package/dist/agent/cli/main.cjs +265 -43
- package/dist/agent/cli/main.d.ts +99 -0
- package/dist/agent/cli/main.js +238 -16
- package/dist/agent/cli/main.mjs +238 -16
- package/dist/agent/index.cjs +18 -2
- package/dist/agent/index.d.ts +60 -482
- package/dist/agent/index.js +17 -1
- package/dist/agent/index.mjs +17 -1
- package/dist/{chunk-J4F5KC3U.js → chunk-AI3M6624.js} +1260 -45
- package/dist/{chunk-IHY2EJTT.cjs → chunk-EX4VEKUF.cjs} +1340 -125
- package/dist/{chunk-6BNZS2A4.mjs → chunk-Q57ENQUW.mjs} +1260 -45
- package/dist/nodeWorkspaceDiscovery-Ami1UkMt.d.ts +631 -0
- package/package.json +1 -1
- package/wasm/pkg/package.json +1 -1
|
@@ -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 =
|
|
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
|
-
|
|
1637
|
-
|
|
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) => ({
|
|
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
|
|
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
|
-
|
|
1892
|
-
(
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
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
|
-
|
|
1908
|
-
|
|
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) =>
|
|
1918
|
-
|
|
1919
|
-
(
|
|
1920
|
-
|
|
1921
|
-
(
|
|
1922
|
-
|
|
1923
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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,
|