@bahulam/code 0.1.7 → 0.1.9
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/README.md +1 -1
- package/package.json +3 -3
- package/src/auth/{tarang-auth.mjs → bahulam-auth.mjs} +1 -1
- package/src/commands/agent.mjs +3 -3
- package/src/commands/device.mjs +2 -2
- package/src/commands/pair.mjs +2 -2
- package/src/commands/remote.mjs +3 -3
- package/src/commands/workflow.mjs +5 -5
- package/src/config/env.mjs +9 -2
- package/src/config/memory-loader.mjs +1 -1
- package/src/config/settings.mjs +2 -2
- package/src/core/agent-loop.mjs +17 -0
- package/src/core/approval.mjs +1 -1
- package/src/core/attachments.mjs +1 -1
- package/src/core/backend-url.mjs +11 -12
- package/src/core/callback-client.mjs +1 -1
- package/src/core/headless.mjs +4 -4
- package/src/core/jsonl-writer.mjs +15 -15
- package/src/core/local-agent.mjs +26 -5
- package/src/core/local-store.mjs +172 -24
- package/src/core/mode-selector.mjs +1 -1
- package/src/core/output-filter.mjs +1 -1
- package/src/core/project-artifacts.mjs +3 -3
- package/src/core/project-context-loader.mjs +9 -9
- package/src/core/settings-sync.mjs +1 -1
- package/src/core/stagnation.mjs +61 -1
- package/src/core/stream-client.mjs +4 -4
- package/src/core/system-prompt.mjs +2 -2
- package/src/core/tool-executor.mjs +79 -61
- package/src/local-service/agent-relay.mjs +8 -8
- package/src/local-service/server.mjs +2 -2
- package/src/mcp/client.mjs +4 -4
- package/src/onboarding/preflight.mjs +1 -1
- package/src/terminal/agents.mjs +2 -2
- package/src/terminal/analytics.mjs +4 -1
- package/src/terminal/main.mjs +6 -6
- package/src/terminal/repl-render.mjs +26 -4
- package/src/terminal/repl-resume.mjs +3 -3
- package/src/terminal/repl.mjs +18 -18
- package/src/terminal/tool-display.mjs +1 -1
- package/src/tools/agent.mjs +2 -2
- package/src/tools/analyze-image.mjs +2 -2
- package/src/tools/generate-image.mjs +4 -4
- package/src/tools/project-overview.mjs +12 -12
- package/src/ui/formatter.mjs +2 -2
- package/src/ui/input-dock.mjs +7 -7
- package/src/ui/spinner.mjs +1 -1
- package/src/ui/term.mjs +2 -6
- package/src/ui/tool-card.mjs +74 -3
package/src/core/local-store.mjs
CHANGED
|
@@ -12,6 +12,100 @@ import { bahulamHome } from './paths.mjs';
|
|
|
12
12
|
|
|
13
13
|
const KEPLER_DIR = bahulamHome();
|
|
14
14
|
const PROJECTS_DIR = path.join(KEPLER_DIR, 'projects');
|
|
15
|
+
const REPLAY_EVENT_RECORD_TYPES = new Set(['bahulam_event', 'kepler_event']);
|
|
16
|
+
|
|
17
|
+
function finiteNumber(value) {
|
|
18
|
+
const n = Number(value);
|
|
19
|
+
return Number.isFinite(n) ? n : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function firstFiniteNumber(...values) {
|
|
23
|
+
for (const value of values) {
|
|
24
|
+
const n = finiteNumber(value);
|
|
25
|
+
if (n !== null) return n;
|
|
26
|
+
}
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function replayEventFromRecord(record) {
|
|
31
|
+
if (!record || !REPLAY_EVENT_RECORD_TYPES.has(record.type) || !record.event) return null;
|
|
32
|
+
const event = record.event;
|
|
33
|
+
if (!event || typeof event !== 'object' || !event.type) return null;
|
|
34
|
+
return {
|
|
35
|
+
...event,
|
|
36
|
+
data: event.data && typeof event.data === 'object' ? event.data : {},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function eventUsage(event) {
|
|
41
|
+
const data = event?.data && typeof event.data === 'object' ? event.data : {};
|
|
42
|
+
const usage = data.usage && typeof data.usage === 'object' ? data.usage : null;
|
|
43
|
+
if (!usage) return null;
|
|
44
|
+
return usage;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function usageTotals(usage = {}) {
|
|
48
|
+
return {
|
|
49
|
+
inputTokens: firstFiniteNumber(usage.total_input_tokens, usage.input_tokens, usage.prompt_tokens),
|
|
50
|
+
outputTokens: firstFiniteNumber(usage.total_output_tokens, usage.output_tokens, usage.completion_tokens),
|
|
51
|
+
cacheReadTokens: firstFiniteNumber(usage.cache_read_input_tokens, usage.cache_read_tokens, usage.cache_read),
|
|
52
|
+
cacheCreationTokens: firstFiniteNumber(usage.cache_creation_input_tokens, usage.cache_creation_tokens, usage.cache_creation),
|
|
53
|
+
reasoningTokens: firstFiniteNumber(usage.reasoning_tokens),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function addModelUsage(meta, modelSet, usage = {}) {
|
|
58
|
+
if (!Array.isArray(usage.models)) return;
|
|
59
|
+
for (const item of usage.models) {
|
|
60
|
+
const model = typeof item === 'string' ? item : item?.model;
|
|
61
|
+
if (typeof model === 'string' && model) modelSet.add(model);
|
|
62
|
+
if (!model || typeof item !== 'object') continue;
|
|
63
|
+
if (!meta.modelUsage[model]) {
|
|
64
|
+
meta.modelUsage[model] = {
|
|
65
|
+
inputTokens: 0,
|
|
66
|
+
outputTokens: 0,
|
|
67
|
+
cacheReadTokens: 0,
|
|
68
|
+
cacheCreationTokens: 0,
|
|
69
|
+
reasoningTokens: 0,
|
|
70
|
+
costUsd: 0,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const totals = usageTotals(item);
|
|
74
|
+
meta.modelUsage[model].inputTokens += totals.inputTokens;
|
|
75
|
+
meta.modelUsage[model].outputTokens += totals.outputTokens;
|
|
76
|
+
meta.modelUsage[model].cacheReadTokens += totals.cacheReadTokens;
|
|
77
|
+
meta.modelUsage[model].cacheCreationTokens += totals.cacheCreationTokens;
|
|
78
|
+
meta.modelUsage[model].reasoningTokens += totals.reasoningTokens;
|
|
79
|
+
meta.modelUsage[model].costUsd += firstFiniteNumber(item.cost_usd, item.cost);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function addUsageTotals(meta, modelSet, usage = {}) {
|
|
84
|
+
const totals = usageTotals(usage);
|
|
85
|
+
meta.inputTokens += totals.inputTokens;
|
|
86
|
+
meta.outputTokens += totals.outputTokens;
|
|
87
|
+
meta.cacheReadTokens += totals.cacheReadTokens;
|
|
88
|
+
meta.cacheCreationTokens += totals.cacheCreationTokens;
|
|
89
|
+
meta.reasoningTokens += totals.reasoningTokens;
|
|
90
|
+
addModelUsage(meta, modelSet, usage);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function assistantHasMatchingComplete(assistantRecord, completeRecords) {
|
|
94
|
+
return completeRecords.some((record) => {
|
|
95
|
+
const distance = Math.abs(Number(assistantRecord.order) - Number(record.order));
|
|
96
|
+
return distance > 0 && distance <= 3;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyUsageRecords(meta, modelSet, records) {
|
|
101
|
+
const completeRecords = records.filter(record => record.source === 'complete');
|
|
102
|
+
for (const record of completeRecords) addUsageTotals(meta, modelSet, record.usage);
|
|
103
|
+
for (const record of records) {
|
|
104
|
+
if (record.source !== 'assistant') continue;
|
|
105
|
+
if (assistantHasMatchingComplete(record, completeRecords)) continue;
|
|
106
|
+
addUsageTotals(meta, modelSet, record.usage);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
15
109
|
|
|
16
110
|
function normalizeBlock(block) {
|
|
17
111
|
if (!block || typeof block !== 'object') {
|
|
@@ -188,8 +282,10 @@ async function parseSessionMeta(filePath) {
|
|
|
188
282
|
outputTokens: 0,
|
|
189
283
|
cacheReadTokens: 0,
|
|
190
284
|
cacheCreationTokens: 0,
|
|
285
|
+
reasoningTokens: 0,
|
|
191
286
|
toolCalls: [], // [{name, count}]
|
|
192
287
|
models: [], // [model strings]
|
|
288
|
+
modelUsage: {}, // model -> token/cost totals from complete events
|
|
193
289
|
modelLimits: {}, // role -> {model, context_length, max_output, source}
|
|
194
290
|
subAgentModels: {}, // role -> model from backend session_info
|
|
195
291
|
startTime: null,
|
|
@@ -207,6 +303,7 @@ async function parseSessionMeta(filePath) {
|
|
|
207
303
|
|
|
208
304
|
const toolCounts = {};
|
|
209
305
|
const modelSet = new Set();
|
|
306
|
+
const usageRecords = [];
|
|
210
307
|
|
|
211
308
|
// endStatus tracking
|
|
212
309
|
let lastMessageRole = null;
|
|
@@ -218,8 +315,10 @@ async function parseSessionMeta(filePath) {
|
|
|
218
315
|
|
|
219
316
|
const fileStream = fs.createReadStream(filePath, { encoding: 'utf-8' });
|
|
220
317
|
const rl = readline.createInterface({ input: fileStream, crlfDelay: Infinity });
|
|
318
|
+
let lineOrder = 0;
|
|
221
319
|
|
|
222
320
|
for await (const line of rl) {
|
|
321
|
+
const recordOrder = lineOrder++;
|
|
223
322
|
if (!line.trim()) continue;
|
|
224
323
|
let obj;
|
|
225
324
|
try { obj = JSON.parse(line); }
|
|
@@ -235,13 +334,30 @@ async function parseSessionMeta(filePath) {
|
|
|
235
334
|
if (!meta.endTime || ts > meta.endTime) meta.endTime = ts;
|
|
236
335
|
}
|
|
237
336
|
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
337
|
+
// Bahulam replay events may carry cost / error markers. Older local
|
|
338
|
+
// transcripts used the same payload under the legacy kepler_event type.
|
|
339
|
+
const ev = replayEventFromRecord(obj);
|
|
340
|
+
if (ev) {
|
|
341
|
+
const data = ev.data || {};
|
|
342
|
+
if (ev.type === 'complete') {
|
|
343
|
+
const usage = eventUsage(ev);
|
|
344
|
+
if (usage) {
|
|
345
|
+
usageRecords.push({ source: 'complete', order: recordOrder, usage });
|
|
346
|
+
}
|
|
347
|
+
const eventCost = firstFiniteNumber(
|
|
348
|
+
data.cost_usd,
|
|
349
|
+
data.total_cost_usd,
|
|
350
|
+
data.total_cost,
|
|
351
|
+
data.usage?.total_cost_usd,
|
|
352
|
+
data.usage?.total_cost,
|
|
353
|
+
data.usage?.cost,
|
|
354
|
+
ev.cost_usd,
|
|
355
|
+
);
|
|
356
|
+
if (eventCost) meta.costUsd += eventCost;
|
|
357
|
+
}
|
|
242
358
|
if (ev.type === 'session_info') {
|
|
243
|
-
if (typeof
|
|
244
|
-
const info =
|
|
359
|
+
if (typeof data.total_cost_usd === 'number') meta.costUsd = data.total_cost_usd;
|
|
360
|
+
const info = data;
|
|
245
361
|
if (info.model_limits && typeof info.model_limits === 'object') {
|
|
246
362
|
meta.modelLimits = info.model_limits;
|
|
247
363
|
}
|
|
@@ -255,15 +371,15 @@ async function parseSessionMeta(filePath) {
|
|
|
255
371
|
}
|
|
256
372
|
}
|
|
257
373
|
if (ev.type === 'error' || ev.error === true) hadError = true;
|
|
258
|
-
if (ev.type === 'resume_summary' && typeof
|
|
374
|
+
if (ev.type === 'resume_summary' && typeof data.summary === 'string') {
|
|
259
375
|
meta.resumeSummary = {
|
|
260
|
-
sourceMessageCount: Number(
|
|
261
|
-
previousSourceMessageCount: Number(
|
|
262
|
-
fullMessageCount: Number(
|
|
263
|
-
summaryChars:
|
|
264
|
-
summarySource:
|
|
265
|
-
mode:
|
|
266
|
-
modeLabel:
|
|
376
|
+
sourceMessageCount: Number(data.source_message_count) || 0,
|
|
377
|
+
previousSourceMessageCount: Number(data.previous_source_message_count) || 0,
|
|
378
|
+
fullMessageCount: Number(data.full_message_count) || 0,
|
|
379
|
+
summaryChars: data.summary.length,
|
|
380
|
+
summarySource: data.summary_source || '',
|
|
381
|
+
mode: data.mode || '',
|
|
382
|
+
modeLabel: data.mode_label || '',
|
|
267
383
|
timestamp: obj.timestamp || null,
|
|
268
384
|
};
|
|
269
385
|
}
|
|
@@ -296,10 +412,7 @@ async function parseSessionMeta(filePath) {
|
|
|
296
412
|
lastMessageRole = 'assistant';
|
|
297
413
|
const usage = obj.message?.usage;
|
|
298
414
|
if (usage) {
|
|
299
|
-
|
|
300
|
-
meta.outputTokens += usage.output_tokens || 0;
|
|
301
|
-
meta.cacheReadTokens += usage.cache_read_input_tokens || 0;
|
|
302
|
-
meta.cacheCreationTokens += usage.cache_creation_input_tokens || 0;
|
|
415
|
+
usageRecords.push({ source: 'assistant', order: recordOrder, usage });
|
|
303
416
|
}
|
|
304
417
|
const model = obj.message?.model;
|
|
305
418
|
if (model) modelSet.add(model);
|
|
@@ -322,6 +435,8 @@ async function parseSessionMeta(filePath) {
|
|
|
322
435
|
.map(([name, count]) => ({ name, count }))
|
|
323
436
|
.sort((a, b) => b.count - a.count);
|
|
324
437
|
meta.models = [...modelSet];
|
|
438
|
+
applyUsageRecords(meta, modelSet, usageRecords);
|
|
439
|
+
meta.models = [...modelSet];
|
|
325
440
|
|
|
326
441
|
// Projected context size for resume should estimate the serialized payload,
|
|
327
442
|
// not cumulative provider usage. Provider input tokens are charged per turn
|
|
@@ -393,11 +508,12 @@ export async function getSessionDetail(sessionId, options = {}) {
|
|
|
393
508
|
}
|
|
394
509
|
const entryOrder = order++;
|
|
395
510
|
|
|
396
|
-
|
|
511
|
+
const replayEvent = replayEventFromRecord(obj);
|
|
512
|
+
if (replayEvent) {
|
|
397
513
|
replayEvents.push({
|
|
398
514
|
order: entryOrder,
|
|
399
515
|
timestamp: obj.timestamp || null,
|
|
400
|
-
event:
|
|
516
|
+
event: replayEvent,
|
|
401
517
|
});
|
|
402
518
|
continue;
|
|
403
519
|
}
|
|
@@ -765,6 +881,7 @@ export async function getSessionStats(days = 30) {
|
|
|
765
881
|
totalInputTokens: 0,
|
|
766
882
|
totalOutputTokens: 0,
|
|
767
883
|
totalCacheReadTokens: 0,
|
|
884
|
+
totalReasoningTokens: 0,
|
|
768
885
|
totalToolCalls: 0,
|
|
769
886
|
toolBreakdown: {},
|
|
770
887
|
modelBreakdown: {},
|
|
@@ -777,13 +894,44 @@ export async function getSessionStats(days = 30) {
|
|
|
777
894
|
stats.totalInputTokens += meta.inputTokens;
|
|
778
895
|
stats.totalOutputTokens += meta.outputTokens;
|
|
779
896
|
stats.totalCacheReadTokens += meta.cacheReadTokens;
|
|
897
|
+
stats.totalReasoningTokens += meta.reasoningTokens;
|
|
780
898
|
|
|
781
899
|
for (const tc of meta.toolCalls) {
|
|
782
900
|
stats.toolBreakdown[tc.name] = (stats.toolBreakdown[tc.name] || 0) + tc.count;
|
|
783
901
|
stats.totalToolCalls += tc.count;
|
|
784
902
|
}
|
|
785
903
|
for (const model of meta.models) {
|
|
786
|
-
|
|
904
|
+
if (!stats.modelBreakdown[model]) {
|
|
905
|
+
stats.modelBreakdown[model] = {
|
|
906
|
+
sessions: 0,
|
|
907
|
+
inputTokens: 0,
|
|
908
|
+
outputTokens: 0,
|
|
909
|
+
cacheReadTokens: 0,
|
|
910
|
+
cacheCreationTokens: 0,
|
|
911
|
+
reasoningTokens: 0,
|
|
912
|
+
costUsd: 0,
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
stats.modelBreakdown[model].sessions += 1;
|
|
916
|
+
}
|
|
917
|
+
for (const [model, usage] of Object.entries(meta.modelUsage || {})) {
|
|
918
|
+
if (!stats.modelBreakdown[model]) {
|
|
919
|
+
stats.modelBreakdown[model] = {
|
|
920
|
+
sessions: 0,
|
|
921
|
+
inputTokens: 0,
|
|
922
|
+
outputTokens: 0,
|
|
923
|
+
cacheReadTokens: 0,
|
|
924
|
+
cacheCreationTokens: 0,
|
|
925
|
+
reasoningTokens: 0,
|
|
926
|
+
costUsd: 0,
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
stats.modelBreakdown[model].inputTokens += usage.inputTokens || 0;
|
|
930
|
+
stats.modelBreakdown[model].outputTokens += usage.outputTokens || 0;
|
|
931
|
+
stats.modelBreakdown[model].cacheReadTokens += usage.cacheReadTokens || 0;
|
|
932
|
+
stats.modelBreakdown[model].cacheCreationTokens += usage.cacheCreationTokens || 0;
|
|
933
|
+
stats.modelBreakdown[model].reasoningTokens += usage.reasoningTokens || 0;
|
|
934
|
+
stats.modelBreakdown[model].costUsd += usage.costUsd || 0;
|
|
787
935
|
}
|
|
788
936
|
}
|
|
789
937
|
|
|
@@ -808,8 +956,8 @@ export async function getToolBreakdown(days = 30) {
|
|
|
808
956
|
export async function getModelBreakdown(days = 30) {
|
|
809
957
|
const stats = await getSessionStats(days);
|
|
810
958
|
return Object.entries(stats.modelBreakdown)
|
|
811
|
-
.map(([model,
|
|
812
|
-
.sort((a, b) => b.sessions - a.sessions);
|
|
959
|
+
.map(([model, usage]) => ({ model, ...usage }))
|
|
960
|
+
.sort((a, b) => b.sessions - a.sessions || (b.inputTokens + b.outputTokens) - (a.inputTokens + a.outputTokens));
|
|
813
961
|
}
|
|
814
962
|
|
|
815
963
|
/**
|
|
@@ -833,7 +981,7 @@ export function getHistory(n = 50) {
|
|
|
833
981
|
|
|
834
982
|
export function getStorePaths() {
|
|
835
983
|
return {
|
|
836
|
-
|
|
984
|
+
bahulamDir: KEPLER_DIR,
|
|
837
985
|
projectsDir: PROJECTS_DIR,
|
|
838
986
|
historyPath: path.join(KEPLER_DIR, 'history.jsonl'),
|
|
839
987
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mode Selector
|
|
3
3
|
*
|
|
4
|
-
* remote (default): All requests go to
|
|
4
|
+
* remote (default): All requests go to Bahulam backend.
|
|
5
5
|
* Backend handles orchestration, model selection, tool routing.
|
|
6
6
|
* User's provider and models configured via web Settings page.
|
|
7
7
|
*
|
|
@@ -23,9 +23,9 @@ export function persistProjectArtifacts(data, resources, log = () => {}) {
|
|
|
23
23
|
for (const [field, filename] of artifacts) {
|
|
24
24
|
try {
|
|
25
25
|
// Resolver honors legacy .kepler/ when it's the only dir that exists.
|
|
26
|
-
const
|
|
27
|
-
fs.mkdirSync(
|
|
28
|
-
const artifactPath = path.join(
|
|
26
|
+
const bahulamDir = projectConfigDir(resource.root);
|
|
27
|
+
fs.mkdirSync(bahulamDir, { recursive: true });
|
|
28
|
+
const artifactPath = path.join(bahulamDir, filename);
|
|
29
29
|
fs.writeFileSync(artifactPath, data[field], 'utf-8');
|
|
30
30
|
resource[field] = data[field];
|
|
31
31
|
written.push(artifactPath);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as crypto from 'node:crypto';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
3
|
import * as path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { loadBahulamMemory } from '../config/memory-loader.mjs';
|
|
5
5
|
import { bahulamHome, projectConfigDir } from './paths.mjs';
|
|
6
6
|
|
|
7
7
|
function sha(content) {
|
|
@@ -27,8 +27,8 @@ function readFile(filePath, label, maxChars = 12000) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
function readTasks(
|
|
31
|
-
const tasksDir = path.join(
|
|
30
|
+
function readTasks(bahulamDir) {
|
|
31
|
+
const tasksDir = path.join(bahulamDir, 'tasks');
|
|
32
32
|
const files = [];
|
|
33
33
|
try {
|
|
34
34
|
if (!fs.existsSync(tasksDir)) return files;
|
|
@@ -66,9 +66,9 @@ function scanSkills(dir, scope) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}) {
|
|
69
|
-
const
|
|
69
|
+
const bahulamDir = projectConfigDir(cwd);
|
|
70
70
|
const files = [];
|
|
71
|
-
for (const file of
|
|
71
|
+
for (const file of loadBahulamMemory({ cwd })) {
|
|
72
72
|
const label = file.path.endsWith(path.join('.bahulam', 'KEPLER.md'))
|
|
73
73
|
? 'KEPLER.md'
|
|
74
74
|
: path.basename(file.path);
|
|
@@ -83,10 +83,10 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
for (const name of ['config.json', 'project.md', 'style.md', 'goal.md', 'plan.md', 'hitl.md']) {
|
|
86
|
-
const file = readFile(path.join(
|
|
86
|
+
const file = readFile(path.join(bahulamDir, name), name, name.endsWith('.json') ? 4000 : 12000);
|
|
87
87
|
if (file) files.push(file);
|
|
88
88
|
}
|
|
89
|
-
files.push(...readTasks(
|
|
89
|
+
files.push(...readTasks(bahulamDir));
|
|
90
90
|
|
|
91
91
|
const previousHashes = new Map((previous?.files || []).map(f => [f.path, f.hash]));
|
|
92
92
|
const changed = files.filter(f => f.hash && previousHashes.get(f.path) && previousHashes.get(f.path) !== f.hash);
|
|
@@ -102,14 +102,14 @@ export function loadProjectContext({ cwd = process.cwd(), previous = null } = {}
|
|
|
102
102
|
|
|
103
103
|
const skills = [
|
|
104
104
|
...scanSkills(bahulamHome(), 'global'),
|
|
105
|
-
...scanSkills(
|
|
105
|
+
...scanSkills(bahulamDir, 'project'),
|
|
106
106
|
];
|
|
107
107
|
const byName = new Map();
|
|
108
108
|
for (const skill of skills) byName.set(skill.name, skill);
|
|
109
109
|
|
|
110
110
|
return {
|
|
111
111
|
root: cwd,
|
|
112
|
-
kepler_dir:
|
|
112
|
+
kepler_dir: bahulamDir,
|
|
113
113
|
files,
|
|
114
114
|
loaded,
|
|
115
115
|
changed,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings Sync — fetch user settings from
|
|
2
|
+
* Settings Sync — fetch user settings from Bahulam web and cache locally.
|
|
3
3
|
*
|
|
4
4
|
* Syncs: gateway_type, model preferences, configured providers.
|
|
5
5
|
* Cached in ~/.bahulam/config.json alongside auth token.
|
package/src/core/stagnation.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const DEFAULT_STAGNATION_THRESHOLD = 3;
|
|
2
|
+
const NO_CHANGE_EDIT_TOOLS = new Set(['edit_file']);
|
|
2
3
|
|
|
3
4
|
function canonicalize(value) {
|
|
4
5
|
if (Array.isArray(value)) return value.map(canonicalize);
|
|
@@ -12,6 +13,26 @@ function canonicalize(value) {
|
|
|
12
13
|
return value;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
function editTarget(input = {}, result = {}) {
|
|
17
|
+
return String(
|
|
18
|
+
input.file_path || input.path || result.file_path || result.path || '',
|
|
19
|
+
).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isNoChangeEdit(tool, result = {}) {
|
|
23
|
+
if (!NO_CHANGE_EDIT_TOOLS.has(tool)) return false;
|
|
24
|
+
if (!result || typeof result !== 'object') return false;
|
|
25
|
+
if (result._no_change || result.no_change) return true;
|
|
26
|
+
if (
|
|
27
|
+
result.success !== false
|
|
28
|
+
&& result.lines_added === 0
|
|
29
|
+
&& result.lines_removed === 0
|
|
30
|
+
) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
export function createStagnationTracker({
|
|
16
37
|
enabled = true,
|
|
17
38
|
threshold = DEFAULT_STAGNATION_THRESHOLD,
|
|
@@ -21,6 +42,8 @@ export function createStagnationTracker({
|
|
|
21
42
|
: DEFAULT_STAGNATION_THRESHOLD;
|
|
22
43
|
let previousSignature = null;
|
|
23
44
|
let consecutiveCount = 0;
|
|
45
|
+
let previousResultSignature = null;
|
|
46
|
+
let consecutiveResultCount = 0;
|
|
24
47
|
|
|
25
48
|
return {
|
|
26
49
|
record(tool, input) {
|
|
@@ -43,14 +66,51 @@ export function createStagnationTracker({
|
|
|
43
66
|
};
|
|
44
67
|
},
|
|
45
68
|
|
|
69
|
+
recordResult(tool, input, result) {
|
|
70
|
+
if (!enabled) return { detected: false, count: 0 };
|
|
71
|
+
if (!isNoChangeEdit(tool, result)) {
|
|
72
|
+
previousResultSignature = null;
|
|
73
|
+
consecutiveResultCount = 0;
|
|
74
|
+
return { detected: false, count: 0 };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const target = editTarget(input, result);
|
|
78
|
+
const signature = JSON.stringify({
|
|
79
|
+
kind: 'no_change_edit',
|
|
80
|
+
tool,
|
|
81
|
+
target,
|
|
82
|
+
});
|
|
83
|
+
if (signature === previousResultSignature) {
|
|
84
|
+
consecutiveResultCount++;
|
|
85
|
+
} else {
|
|
86
|
+
previousResultSignature = signature;
|
|
87
|
+
consecutiveResultCount = 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
detected: consecutiveResultCount >= effectiveThreshold,
|
|
92
|
+
count: consecutiveResultCount,
|
|
93
|
+
kind: 'no_change_edit',
|
|
94
|
+
target,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
|
|
46
98
|
reset() {
|
|
47
99
|
previousSignature = null;
|
|
48
100
|
consecutiveCount = 0;
|
|
101
|
+
previousResultSignature = null;
|
|
102
|
+
consecutiveResultCount = 0;
|
|
49
103
|
},
|
|
50
104
|
};
|
|
51
105
|
}
|
|
52
106
|
|
|
53
|
-
export function stagnationMessage(tool, count) {
|
|
107
|
+
export function stagnationMessage(tool, count, details = {}) {
|
|
108
|
+
if (details.kind === 'no_change_edit') {
|
|
109
|
+
const target = details.target ? ` for "${details.target}"` : '';
|
|
110
|
+
return `STAGNATION WARNING: Tool "${tool}" made no file changes ${count} consecutive times${target}. ` +
|
|
111
|
+
`The duplicate edit loop was stopped. Re-read the target range, compare the current file content, ` +
|
|
112
|
+
`change the edit strategy, or finish the task.`;
|
|
113
|
+
}
|
|
54
114
|
return `STAGNATION WARNING: Tool "${tool}" was called ${count} consecutive times ` +
|
|
55
115
|
`with identical arguments. The duplicate call was skipped. Review the previous ` +
|
|
56
116
|
`result, change the arguments, try another tool, or finish the task.`;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* BahulamStreamClient — SSE consumer for Bahulam backend.
|
|
3
3
|
*
|
|
4
4
|
* Replaces OCC's agent-loop.mjs. Instead of calling the LLM API directly,
|
|
5
|
-
* this client POSTs to the
|
|
5
|
+
* this client POSTs to the Bahulam backend, parses the SSE stream, intercepts
|
|
6
6
|
* tool_request/tool_call events (executes locally, POSTs callback), and
|
|
7
7
|
* yields all other events to the caller for rendering.
|
|
8
8
|
*
|
|
@@ -117,10 +117,10 @@ function isOfflineLikelyError(err) {
|
|
|
117
117
|
);
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
export class
|
|
120
|
+
export class BahulamStreamClient {
|
|
121
121
|
/**
|
|
122
122
|
* @param {Object} opts
|
|
123
|
-
* @param {string} opts.baseUrl -
|
|
123
|
+
* @param {string} opts.baseUrl - Bahulam backend URL (ignored when mode='bundled')
|
|
124
124
|
* @param {string} opts.token - CLI auth token
|
|
125
125
|
* @param {Object} opts.toolExecutor - { execute(name, args) }
|
|
126
126
|
* @param {boolean} [opts.verbose=false]
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import path from 'path';
|
|
12
12
|
import os from 'os';
|
|
13
|
-
import {
|
|
13
|
+
import { loadBahulamMemory } from '../config/memory-loader.mjs';
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
16
|
* Load all CLAUDE.md files and merge them in order.
|
|
@@ -89,7 +89,7 @@ export function buildSystemPrompt({ cwd, tools, override, addDirs } = {}) {
|
|
|
89
89
|
parts.push(f.content);
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
for (const f of
|
|
92
|
+
for (const f of loadBahulamMemory({ cwd })) {
|
|
93
93
|
parts.push(f.content);
|
|
94
94
|
}
|
|
95
95
|
|