@dreb/coding-agent 2.15.2 → 2.17.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/README.md +4 -3
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -0
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +10 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +51 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/performance-tracker.d.ts +54 -0
- package/dist/core/performance-tracker.d.ts.map +1 -0
- package/dist/core/performance-tracker.js +298 -0
- package/dist/core/performance-tracker.js.map +1 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +1 -1
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/tools/index.d.ts +5 -0
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +8 -0
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/suggest-next.d.ts +19 -0
- package/dist/core/tools/suggest-next.d.ts.map +1 -0
- package/dist/core/tools/suggest-next.js +72 -0
- package/dist/core/tools/suggest-next.js.map +1 -0
- package/dist/core/tools/tmp-read.d.ts.map +1 -1
- package/dist/core/tools/tmp-read.js +14 -1
- package/dist/core/tools/tmp-read.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +24 -2
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +13 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +12 -0
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +7 -0
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts +9 -0
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +7 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +17 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/extensions.md +2 -2
- package/docs/json.md +2 -1
- package/docs/rpc.md +30 -0
- package/docs/session.md +24 -0
- package/docs/tui.md +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export interface PerformanceEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
provider: string;
|
|
5
|
+
modelId: string;
|
|
6
|
+
outputTokens: number;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
tps: number;
|
|
9
|
+
}
|
|
10
|
+
export interface RollingAverage {
|
|
11
|
+
median: number;
|
|
12
|
+
mean: number;
|
|
13
|
+
count: number;
|
|
14
|
+
}
|
|
15
|
+
export type PerformanceDeltaDirection = "above" | "below" | "stable";
|
|
16
|
+
export interface PerformanceDelta {
|
|
17
|
+
baselineMedian: number;
|
|
18
|
+
recentMedian: number;
|
|
19
|
+
percentDelta: number;
|
|
20
|
+
direction: PerformanceDeltaDirection;
|
|
21
|
+
baselineCount: number;
|
|
22
|
+
recentCount: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class PerformanceTracker {
|
|
25
|
+
private static readonly PRUNE_INTERVAL_MS;
|
|
26
|
+
private static readonly LOCK_TIMEOUT_MS;
|
|
27
|
+
private static readonly PRUNE_LOCK_TIMEOUT_MS;
|
|
28
|
+
private static readonly STALE_LOCK_MS;
|
|
29
|
+
private logPath;
|
|
30
|
+
private lockPath;
|
|
31
|
+
private entries;
|
|
32
|
+
private pruneTimer;
|
|
33
|
+
private disposed;
|
|
34
|
+
constructor(logPath?: string);
|
|
35
|
+
record(entry: PerformanceEntry): void;
|
|
36
|
+
getRollingAverage(provider: string, modelId: string, count?: number): RollingAverage;
|
|
37
|
+
getPerformanceDelta(provider: string, modelId: string, recentCount?: number, baselineCount?: number, stablePercent?: number): PerformanceDelta;
|
|
38
|
+
getAllRollingAverages(windowMs?: number): Array<{
|
|
39
|
+
provider: string;
|
|
40
|
+
modelId: string;
|
|
41
|
+
median: number;
|
|
42
|
+
mean: number;
|
|
43
|
+
count: number;
|
|
44
|
+
}>;
|
|
45
|
+
prune(ageMs?: number): void;
|
|
46
|
+
dispose(): void;
|
|
47
|
+
private ensureDir;
|
|
48
|
+
private schedulePrune;
|
|
49
|
+
private readEntries;
|
|
50
|
+
private withLogLock;
|
|
51
|
+
private acquireLock;
|
|
52
|
+
private removeStaleLock;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=performance-tracker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-tracker.d.ts","sourceRoot":"","sources":["../../src/core/performance-tracker.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,cAAc;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,yBAAyB,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAErE,MAAM,WAAW,gBAAgB;IAChC,cAAc,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,yBAAyB,CAAC;IACrC,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,kBAAkB;IAC9B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAuB;IAChE,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAQ;IAC/C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAAQ;IACrD,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAiB;IAEtD,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,OAAO,CAAqB;IACpC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAS;IAEzB,YAAY,OAAO,CAAC,EAAE,MAAM,EAU3B;IAED,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,IAAI,CAepC;IAED,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,SAAM,GAAG,cAAc,CAgBhF;IAED,mBAAmB,CAClB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EACf,WAAW,SAAK,EAChB,aAAa,SAAS,EACtB,aAAa,SAAI,GACf,gBAAgB,CAiClB;IAED,qBAAqB,CACpB,QAAQ,SAAsB,GAC5B,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAyB3F;IAED,KAAK,CAAC,KAAK,SAA2B,GAAG,IAAI,CAsC5C;IAED,OAAO,IAAI,IAAI,CAMd;IAED,OAAO,CAAC,SAAS;IAWjB,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,WAAW;IA+BnB,OAAO,CAAC,WAAW;IAcnB,OAAO,CAAC,WAAW;IAgBnB,OAAO,CAAC,eAAe;CAUvB","sourcesContent":["import {\n\tappendFileSync,\n\tcloseSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n\twriteFileSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport { getPerformanceLogPath } from \"../config.js\";\n\nexport interface PerformanceEntry {\n\ttimestamp: string;\n\tsessionId: string;\n\tprovider: string;\n\tmodelId: string;\n\toutputTokens: number;\n\tdurationMs: number;\n\ttps: number;\n}\n\nexport interface RollingAverage {\n\tmedian: number;\n\tmean: number;\n\tcount: number;\n}\n\nexport type PerformanceDeltaDirection = \"above\" | \"below\" | \"stable\";\n\nexport interface PerformanceDelta {\n\tbaselineMedian: number;\n\trecentMedian: number;\n\tpercentDelta: number;\n\tdirection: PerformanceDeltaDirection;\n\tbaselineCount: number;\n\trecentCount: number;\n}\n\nexport class PerformanceTracker {\n\tprivate static readonly PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\tprivate static readonly LOCK_TIMEOUT_MS = 1000;\n\tprivate static readonly PRUNE_LOCK_TIMEOUT_MS = 5000;\n\tprivate static readonly STALE_LOCK_MS = 5 * 60 * 1000;\n\n\tprivate logPath: string;\n\tprivate lockPath: string;\n\tprivate entries: PerformanceEntry[];\n\tprivate pruneTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate disposed = false;\n\n\tconstructor(logPath?: string) {\n\t\tthis.logPath = logPath ?? getPerformanceLogPath();\n\t\tthis.lockPath = `${this.logPath}.lock`;\n\t\ttry {\n\t\t\tthis.entries = this.readEntries();\n\t\t} catch {\n\t\t\tthis.entries = [];\n\t\t}\n\t\tthis.ensureDir();\n\t\tthis.schedulePrune();\n\t}\n\n\trecord(entry: PerformanceEntry): void {\n\t\tif (this.disposed) return;\n\t\ttry {\n\t\t\tconst wrote = this.withLogLock(() => {\n\t\t\t\tappendFileSync(this.logPath, `${JSON.stringify(entry)}\\n`, \"utf8\");\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\tif (!wrote) {\n\t\t\t\tconsole.warn(\"[PerformanceTracker] Failed to write performance entry: could not acquire log lock\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.entries.push(entry);\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[PerformanceTracker] Failed to write performance entry: ${error}`);\n\t\t}\n\t}\n\n\tgetRollingAverage(provider: string, modelId: string, count = 100): RollingAverage {\n\t\tconst values = this.entries\n\t\t\t.filter((e) => e.provider === provider && e.modelId === modelId)\n\t\t\t.sort((a, b) => entryTime(b) - entryTime(a))\n\t\t\t.slice(0, count)\n\t\t\t.map((e) => e.tps);\n\n\t\tif (values.length === 0) {\n\t\t\treturn { median: 0, mean: 0, count: 0 };\n\t\t}\n\n\t\treturn {\n\t\t\tmedian: computeMedian(values),\n\t\t\tmean: computeMean(values),\n\t\t\tcount: values.length,\n\t\t};\n\t}\n\n\tgetPerformanceDelta(\n\t\tprovider: string,\n\t\tmodelId: string,\n\t\trecentCount = 10,\n\t\tbaselineCount = 10_000,\n\t\tstablePercent = 1,\n\t): PerformanceDelta {\n\t\tconst modelEntries = this.entries\n\t\t\t.filter((e) => e.provider === provider && e.modelId === modelId)\n\t\t\t.sort((a, b) => entryTime(b) - entryTime(a));\n\n\t\tconst baselineSlice = baselineCount > 0 ? baselineCount : modelEntries.length;\n\t\tconst baselineValues = modelEntries.slice(0, baselineSlice).map((e) => e.tps);\n\t\tconst recentValues = modelEntries.slice(0, recentCount).map((e) => e.tps);\n\n\t\tconst baselineMedian = computeMedian(baselineValues);\n\t\tconst recentMedian = computeMedian(recentValues);\n\t\tif (baselineValues.length < 3 || recentValues.length < 3 || baselineMedian <= 0) {\n\t\t\treturn {\n\t\t\t\tbaselineMedian,\n\t\t\t\trecentMedian,\n\t\t\t\tpercentDelta: 0,\n\t\t\t\tdirection: \"stable\",\n\t\t\t\tbaselineCount: baselineValues.length,\n\t\t\t\trecentCount: recentValues.length,\n\t\t\t};\n\t\t}\n\n\t\tconst percentDelta = ((recentMedian - baselineMedian) / baselineMedian) * 100;\n\t\tconst direction = Math.abs(percentDelta) < stablePercent ? \"stable\" : percentDelta > 0 ? \"above\" : \"below\";\n\n\t\treturn {\n\t\t\tbaselineMedian,\n\t\t\trecentMedian,\n\t\t\tpercentDelta,\n\t\t\tdirection,\n\t\t\tbaselineCount: baselineValues.length,\n\t\t\trecentCount: recentValues.length,\n\t\t};\n\t}\n\n\tgetAllRollingAverages(\n\t\twindowMs = 24 * 60 * 60 * 1000,\n\t): Array<{ provider: string; modelId: string; median: number; mean: number; count: number }> {\n\t\tconst cutoff = Date.now() - windowMs;\n\t\tconst filtered = this.entries.filter((e) => entryTime(e) >= cutoff);\n\n\t\tconst groups = new Map<string, number[]>();\n\t\tfor (const entry of filtered) {\n\t\t\tconst key = `${entry.provider}\\0${entry.modelId}`;\n\t\t\tconst arr = groups.get(key) ?? [];\n\t\t\tarr.push(entry.tps);\n\t\t\tgroups.set(key, arr);\n\t\t}\n\n\t\tconst results: Array<{ provider: string; modelId: string; median: number; mean: number; count: number }> = [];\n\t\tfor (const [key, values] of groups) {\n\t\t\tconst [provider, modelId] = key.split(\"\\0\");\n\t\t\tresults.push({\n\t\t\t\tprovider,\n\t\t\t\tmodelId,\n\t\t\t\tmedian: computeMedian(values),\n\t\t\t\tmean: computeMean(values),\n\t\t\t\tcount: values.length,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tprune(ageMs = 30 * 24 * 60 * 60 * 1000): void {\n\t\tif (this.disposed) return;\n\t\tlet tempPath: string | undefined;\n\t\ttry {\n\t\t\tconst pruned = this.withLogLock(() => {\n\t\t\t\tconst cutoff = Date.now() - ageMs;\n\t\t\t\tconst sourceEntries = this.readEntries();\n\t\t\t\tconst kept: PerformanceEntry[] = [];\n\t\t\t\tconst lines: string[] = [];\n\n\t\t\t\tfor (const entry of sourceEntries) {\n\t\t\t\t\tif (entryTime(entry) >= cutoff) {\n\t\t\t\t\t\tkept.push(entry);\n\t\t\t\t\t\tlines.push(JSON.stringify(entry));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttempPath = join(dirname(this.logPath), `.performance-prune-${process.pid}-${Date.now()}.jsonl`);\n\t\t\t\twriteFileSync(tempPath, lines.length > 0 ? `${lines.join(\"\\n\")}\\n` : \"\", \"utf8\");\n\t\t\t\trenameSync(tempPath, this.logPath);\n\t\t\t\ttempPath = undefined;\n\t\t\t\tthis.entries = kept;\n\t\t\t\treturn true;\n\t\t\t}, PerformanceTracker.PRUNE_LOCK_TIMEOUT_MS);\n\t\t\tif (!pruned) {\n\t\t\t\tconsole.warn(\"[PerformanceTracker] Failed to prune performance log: could not acquire log lock\");\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[PerformanceTracker] Failed to prune performance log: ${error}`);\n\t\t} finally {\n\t\t\tif (tempPath) {\n\t\t\t\ttry {\n\t\t\t\t\trmSync(tempPath, { force: true });\n\t\t\t\t} catch {\n\t\t\t\t\t// Best-effort cleanup only\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.pruneTimer) {\n\t\t\tclearInterval(this.pruneTimer);\n\t\t\tthis.pruneTimer = null;\n\t\t}\n\t}\n\n\tprivate ensureDir(): void {\n\t\ttry {\n\t\t\tmkdirSync(dirname(this.logPath), { recursive: true });\n\t\t} catch (error) {\n\t\t\tif (isFileExistsError(error)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.warn(`[PerformanceTracker] Failed to create log directory: ${error}`);\n\t\t}\n\t}\n\n\tprivate schedulePrune(): void {\n\t\tif (this.disposed) return;\n\t\tthis.pruneTimer = setInterval(() => {\n\t\t\tthis.prune();\n\t\t}, PerformanceTracker.PRUNE_INTERVAL_MS);\n\t\tthis.pruneTimer?.unref?.();\n\t}\n\n\tprivate readEntries(): PerformanceEntry[] {\n\t\ttry {\n\t\t\tconst content = readFileSync(this.logPath, \"utf8\");\n\t\t\tconst entries: PerformanceEntry[] = [];\n\t\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\t\tif (!line.trim()) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = JSON.parse(line) as unknown;\n\t\t\t\t\tif (!isValidPerformanceEntry(parsed)) {\n\t\t\t\t\t\tconsole.warn(`[PerformanceTracker] Skipping invalid performance entry line: ${line}`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst time = entryTime(parsed);\n\t\t\t\t\tif (!Number.isFinite(time)) {\n\t\t\t\t\t\tconsole.warn(`[PerformanceTracker] Skipping entry with invalid timestamp: ${line}`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tentries.push(parsed);\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip malformed lines\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn entries;\n\t\t} catch (error) {\n\t\t\tif (isENOENT(error)) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate withLogLock<T>(operation: () => T, timeoutMs = PerformanceTracker.LOCK_TIMEOUT_MS): T | undefined {\n\t\tconst fd = this.acquireLock(timeoutMs);\n\t\tif (fd === undefined) return undefined;\n\t\ttry {\n\t\t\treturn operation();\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tcloseSync(fd);\n\t\t\t} finally {\n\t\t\t\trmSync(this.lockPath, { force: true });\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate acquireLock(timeoutMs: number): number | undefined {\n\t\tconst start = performance.now();\n\t\twhile (performance.now() - start <= timeoutMs) {\n\t\t\ttry {\n\t\t\t\treturn openSync(this.lockPath, \"wx\");\n\t\t\t} catch (error) {\n\t\t\t\tif (!isFileExistsError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tthis.removeStaleLock();\n\t\t\t\tsleepSync(10);\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate removeStaleLock(): void {\n\t\ttry {\n\t\t\tconst lockAgeMs = Date.now() - statSync(this.lockPath).mtimeMs;\n\t\t\tif (lockAgeMs > PerformanceTracker.STALE_LOCK_MS) {\n\t\t\t\trmSync(this.lockPath, { force: true });\n\t\t\t}\n\t\t} catch {\n\t\t\t// Lock disappeared between open attempts\n\t\t}\n\t}\n}\n\nfunction entryTime(entry: PerformanceEntry): number {\n\tconst time = new Date(entry.timestamp).getTime();\n\treturn Number.isFinite(time) ? time : NaN;\n}\n\nfunction computeMedian(values: number[]): number {\n\tif (values.length === 0) return 0;\n\tconst sorted = [...values].sort((a, b) => a - b);\n\tconst mid = Math.floor(sorted.length / 2);\n\tif (sorted.length % 2 === 1) {\n\t\treturn sorted[mid];\n\t}\n\treturn (sorted[mid - 1] + sorted[mid]) / 2;\n}\n\nfunction computeMean(values: number[]): number {\n\tif (values.length === 0) return 0;\n\treturn values.reduce((a, b) => a + b, 0) / values.length;\n}\n\nfunction isFileExistsError(error: unknown): boolean {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"EEXIST\";\n}\n\nfunction isENOENT(error: unknown): boolean {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"ENOENT\";\n}\n\nfunction isValidPerformanceEntry(entry: unknown): entry is PerformanceEntry {\n\tif (typeof entry !== \"object\" || entry === null) return false;\n\tconst e = entry as Record<string, unknown>;\n\treturn (\n\t\ttypeof e.timestamp === \"string\" &&\n\t\ttypeof e.provider === \"string\" &&\n\t\ttypeof e.modelId === \"string\" &&\n\t\ttypeof e.outputTokens === \"number\" &&\n\t\tNumber.isFinite(e.outputTokens) &&\n\t\ttypeof e.durationMs === \"number\" &&\n\t\tNumber.isFinite(e.durationMs) &&\n\t\ttypeof e.tps === \"number\" &&\n\t\tNumber.isFinite(e.tps)\n\t);\n}\n\nfunction sleepSync(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n"]}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { appendFileSync, closeSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "fs";
|
|
2
|
+
import { dirname, join } from "path";
|
|
3
|
+
import { getPerformanceLogPath } from "../config.js";
|
|
4
|
+
export class PerformanceTracker {
|
|
5
|
+
static PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
6
|
+
static LOCK_TIMEOUT_MS = 1000;
|
|
7
|
+
static PRUNE_LOCK_TIMEOUT_MS = 5000;
|
|
8
|
+
static STALE_LOCK_MS = 5 * 60 * 1000;
|
|
9
|
+
logPath;
|
|
10
|
+
lockPath;
|
|
11
|
+
entries;
|
|
12
|
+
pruneTimer = null;
|
|
13
|
+
disposed = false;
|
|
14
|
+
constructor(logPath) {
|
|
15
|
+
this.logPath = logPath ?? getPerformanceLogPath();
|
|
16
|
+
this.lockPath = `${this.logPath}.lock`;
|
|
17
|
+
try {
|
|
18
|
+
this.entries = this.readEntries();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
this.entries = [];
|
|
22
|
+
}
|
|
23
|
+
this.ensureDir();
|
|
24
|
+
this.schedulePrune();
|
|
25
|
+
}
|
|
26
|
+
record(entry) {
|
|
27
|
+
if (this.disposed)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const wrote = this.withLogLock(() => {
|
|
31
|
+
appendFileSync(this.logPath, `${JSON.stringify(entry)}\n`, "utf8");
|
|
32
|
+
return true;
|
|
33
|
+
});
|
|
34
|
+
if (!wrote) {
|
|
35
|
+
console.warn("[PerformanceTracker] Failed to write performance entry: could not acquire log lock");
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
this.entries.push(entry);
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
console.warn(`[PerformanceTracker] Failed to write performance entry: ${error}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
getRollingAverage(provider, modelId, count = 100) {
|
|
45
|
+
const values = this.entries
|
|
46
|
+
.filter((e) => e.provider === provider && e.modelId === modelId)
|
|
47
|
+
.sort((a, b) => entryTime(b) - entryTime(a))
|
|
48
|
+
.slice(0, count)
|
|
49
|
+
.map((e) => e.tps);
|
|
50
|
+
if (values.length === 0) {
|
|
51
|
+
return { median: 0, mean: 0, count: 0 };
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
median: computeMedian(values),
|
|
55
|
+
mean: computeMean(values),
|
|
56
|
+
count: values.length,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
getPerformanceDelta(provider, modelId, recentCount = 10, baselineCount = 10_000, stablePercent = 1) {
|
|
60
|
+
const modelEntries = this.entries
|
|
61
|
+
.filter((e) => e.provider === provider && e.modelId === modelId)
|
|
62
|
+
.sort((a, b) => entryTime(b) - entryTime(a));
|
|
63
|
+
const baselineSlice = baselineCount > 0 ? baselineCount : modelEntries.length;
|
|
64
|
+
const baselineValues = modelEntries.slice(0, baselineSlice).map((e) => e.tps);
|
|
65
|
+
const recentValues = modelEntries.slice(0, recentCount).map((e) => e.tps);
|
|
66
|
+
const baselineMedian = computeMedian(baselineValues);
|
|
67
|
+
const recentMedian = computeMedian(recentValues);
|
|
68
|
+
if (baselineValues.length < 3 || recentValues.length < 3 || baselineMedian <= 0) {
|
|
69
|
+
return {
|
|
70
|
+
baselineMedian,
|
|
71
|
+
recentMedian,
|
|
72
|
+
percentDelta: 0,
|
|
73
|
+
direction: "stable",
|
|
74
|
+
baselineCount: baselineValues.length,
|
|
75
|
+
recentCount: recentValues.length,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const percentDelta = ((recentMedian - baselineMedian) / baselineMedian) * 100;
|
|
79
|
+
const direction = Math.abs(percentDelta) < stablePercent ? "stable" : percentDelta > 0 ? "above" : "below";
|
|
80
|
+
return {
|
|
81
|
+
baselineMedian,
|
|
82
|
+
recentMedian,
|
|
83
|
+
percentDelta,
|
|
84
|
+
direction,
|
|
85
|
+
baselineCount: baselineValues.length,
|
|
86
|
+
recentCount: recentValues.length,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
getAllRollingAverages(windowMs = 24 * 60 * 60 * 1000) {
|
|
90
|
+
const cutoff = Date.now() - windowMs;
|
|
91
|
+
const filtered = this.entries.filter((e) => entryTime(e) >= cutoff);
|
|
92
|
+
const groups = new Map();
|
|
93
|
+
for (const entry of filtered) {
|
|
94
|
+
const key = `${entry.provider}\0${entry.modelId}`;
|
|
95
|
+
const arr = groups.get(key) ?? [];
|
|
96
|
+
arr.push(entry.tps);
|
|
97
|
+
groups.set(key, arr);
|
|
98
|
+
}
|
|
99
|
+
const results = [];
|
|
100
|
+
for (const [key, values] of groups) {
|
|
101
|
+
const [provider, modelId] = key.split("\0");
|
|
102
|
+
results.push({
|
|
103
|
+
provider,
|
|
104
|
+
modelId,
|
|
105
|
+
median: computeMedian(values),
|
|
106
|
+
mean: computeMean(values),
|
|
107
|
+
count: values.length,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return results;
|
|
111
|
+
}
|
|
112
|
+
prune(ageMs = 30 * 24 * 60 * 60 * 1000) {
|
|
113
|
+
if (this.disposed)
|
|
114
|
+
return;
|
|
115
|
+
let tempPath;
|
|
116
|
+
try {
|
|
117
|
+
const pruned = this.withLogLock(() => {
|
|
118
|
+
const cutoff = Date.now() - ageMs;
|
|
119
|
+
const sourceEntries = this.readEntries();
|
|
120
|
+
const kept = [];
|
|
121
|
+
const lines = [];
|
|
122
|
+
for (const entry of sourceEntries) {
|
|
123
|
+
if (entryTime(entry) >= cutoff) {
|
|
124
|
+
kept.push(entry);
|
|
125
|
+
lines.push(JSON.stringify(entry));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
tempPath = join(dirname(this.logPath), `.performance-prune-${process.pid}-${Date.now()}.jsonl`);
|
|
129
|
+
writeFileSync(tempPath, lines.length > 0 ? `${lines.join("\n")}\n` : "", "utf8");
|
|
130
|
+
renameSync(tempPath, this.logPath);
|
|
131
|
+
tempPath = undefined;
|
|
132
|
+
this.entries = kept;
|
|
133
|
+
return true;
|
|
134
|
+
}, PerformanceTracker.PRUNE_LOCK_TIMEOUT_MS);
|
|
135
|
+
if (!pruned) {
|
|
136
|
+
console.warn("[PerformanceTracker] Failed to prune performance log: could not acquire log lock");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
console.warn(`[PerformanceTracker] Failed to prune performance log: ${error}`);
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
if (tempPath) {
|
|
144
|
+
try {
|
|
145
|
+
rmSync(tempPath, { force: true });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Best-effort cleanup only
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
dispose() {
|
|
154
|
+
this.disposed = true;
|
|
155
|
+
if (this.pruneTimer) {
|
|
156
|
+
clearInterval(this.pruneTimer);
|
|
157
|
+
this.pruneTimer = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
ensureDir() {
|
|
161
|
+
try {
|
|
162
|
+
mkdirSync(dirname(this.logPath), { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (isFileExistsError(error)) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
console.warn(`[PerformanceTracker] Failed to create log directory: ${error}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
schedulePrune() {
|
|
172
|
+
if (this.disposed)
|
|
173
|
+
return;
|
|
174
|
+
this.pruneTimer = setInterval(() => {
|
|
175
|
+
this.prune();
|
|
176
|
+
}, PerformanceTracker.PRUNE_INTERVAL_MS);
|
|
177
|
+
this.pruneTimer?.unref?.();
|
|
178
|
+
}
|
|
179
|
+
readEntries() {
|
|
180
|
+
try {
|
|
181
|
+
const content = readFileSync(this.logPath, "utf8");
|
|
182
|
+
const entries = [];
|
|
183
|
+
for (const line of content.split("\n")) {
|
|
184
|
+
if (!line.trim())
|
|
185
|
+
continue;
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(line);
|
|
188
|
+
if (!isValidPerformanceEntry(parsed)) {
|
|
189
|
+
console.warn(`[PerformanceTracker] Skipping invalid performance entry line: ${line}`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const time = entryTime(parsed);
|
|
193
|
+
if (!Number.isFinite(time)) {
|
|
194
|
+
console.warn(`[PerformanceTracker] Skipping entry with invalid timestamp: ${line}`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
entries.push(parsed);
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Skip malformed lines
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return entries;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (isENOENT(error)) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
withLogLock(operation, timeoutMs = PerformanceTracker.LOCK_TIMEOUT_MS) {
|
|
213
|
+
const fd = this.acquireLock(timeoutMs);
|
|
214
|
+
if (fd === undefined)
|
|
215
|
+
return undefined;
|
|
216
|
+
try {
|
|
217
|
+
return operation();
|
|
218
|
+
}
|
|
219
|
+
finally {
|
|
220
|
+
try {
|
|
221
|
+
closeSync(fd);
|
|
222
|
+
}
|
|
223
|
+
finally {
|
|
224
|
+
rmSync(this.lockPath, { force: true });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
acquireLock(timeoutMs) {
|
|
229
|
+
const start = performance.now();
|
|
230
|
+
while (performance.now() - start <= timeoutMs) {
|
|
231
|
+
try {
|
|
232
|
+
return openSync(this.lockPath, "wx");
|
|
233
|
+
}
|
|
234
|
+
catch (error) {
|
|
235
|
+
if (!isFileExistsError(error)) {
|
|
236
|
+
throw error;
|
|
237
|
+
}
|
|
238
|
+
this.removeStaleLock();
|
|
239
|
+
sleepSync(10);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
removeStaleLock() {
|
|
245
|
+
try {
|
|
246
|
+
const lockAgeMs = Date.now() - statSync(this.lockPath).mtimeMs;
|
|
247
|
+
if (lockAgeMs > PerformanceTracker.STALE_LOCK_MS) {
|
|
248
|
+
rmSync(this.lockPath, { force: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Lock disappeared between open attempts
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function entryTime(entry) {
|
|
257
|
+
const time = new Date(entry.timestamp).getTime();
|
|
258
|
+
return Number.isFinite(time) ? time : NaN;
|
|
259
|
+
}
|
|
260
|
+
function computeMedian(values) {
|
|
261
|
+
if (values.length === 0)
|
|
262
|
+
return 0;
|
|
263
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
264
|
+
const mid = Math.floor(sorted.length / 2);
|
|
265
|
+
if (sorted.length % 2 === 1) {
|
|
266
|
+
return sorted[mid];
|
|
267
|
+
}
|
|
268
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
269
|
+
}
|
|
270
|
+
function computeMean(values) {
|
|
271
|
+
if (values.length === 0)
|
|
272
|
+
return 0;
|
|
273
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
274
|
+
}
|
|
275
|
+
function isFileExistsError(error) {
|
|
276
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "EEXIST";
|
|
277
|
+
}
|
|
278
|
+
function isENOENT(error) {
|
|
279
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
280
|
+
}
|
|
281
|
+
function isValidPerformanceEntry(entry) {
|
|
282
|
+
if (typeof entry !== "object" || entry === null)
|
|
283
|
+
return false;
|
|
284
|
+
const e = entry;
|
|
285
|
+
return (typeof e.timestamp === "string" &&
|
|
286
|
+
typeof e.provider === "string" &&
|
|
287
|
+
typeof e.modelId === "string" &&
|
|
288
|
+
typeof e.outputTokens === "number" &&
|
|
289
|
+
Number.isFinite(e.outputTokens) &&
|
|
290
|
+
typeof e.durationMs === "number" &&
|
|
291
|
+
Number.isFinite(e.durationMs) &&
|
|
292
|
+
typeof e.tps === "number" &&
|
|
293
|
+
Number.isFinite(e.tps));
|
|
294
|
+
}
|
|
295
|
+
function sleepSync(ms) {
|
|
296
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
297
|
+
}
|
|
298
|
+
//# sourceMappingURL=performance-tracker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"performance-tracker.js","sourceRoot":"","sources":["../../src/core/performance-tracker.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n\tappendFileSync,\n\tcloseSync,\n\tmkdirSync,\n\topenSync,\n\treadFileSync,\n\trenameSync,\n\trmSync,\n\tstatSync,\n\twriteFileSync,\n} from \"fs\";\nimport { dirname, join } from \"path\";\nimport { getPerformanceLogPath } from \"../config.js\";\n\nexport interface PerformanceEntry {\n\ttimestamp: string;\n\tsessionId: string;\n\tprovider: string;\n\tmodelId: string;\n\toutputTokens: number;\n\tdurationMs: number;\n\ttps: number;\n}\n\nexport interface RollingAverage {\n\tmedian: number;\n\tmean: number;\n\tcount: number;\n}\n\nexport type PerformanceDeltaDirection = \"above\" | \"below\" | \"stable\";\n\nexport interface PerformanceDelta {\n\tbaselineMedian: number;\n\trecentMedian: number;\n\tpercentDelta: number;\n\tdirection: PerformanceDeltaDirection;\n\tbaselineCount: number;\n\trecentCount: number;\n}\n\nexport class PerformanceTracker {\n\tprivate static readonly PRUNE_INTERVAL_MS = 24 * 60 * 60 * 1000;\n\tprivate static readonly LOCK_TIMEOUT_MS = 1000;\n\tprivate static readonly PRUNE_LOCK_TIMEOUT_MS = 5000;\n\tprivate static readonly STALE_LOCK_MS = 5 * 60 * 1000;\n\n\tprivate logPath: string;\n\tprivate lockPath: string;\n\tprivate entries: PerformanceEntry[];\n\tprivate pruneTimer: ReturnType<typeof setInterval> | null = null;\n\tprivate disposed = false;\n\n\tconstructor(logPath?: string) {\n\t\tthis.logPath = logPath ?? getPerformanceLogPath();\n\t\tthis.lockPath = `${this.logPath}.lock`;\n\t\ttry {\n\t\t\tthis.entries = this.readEntries();\n\t\t} catch {\n\t\t\tthis.entries = [];\n\t\t}\n\t\tthis.ensureDir();\n\t\tthis.schedulePrune();\n\t}\n\n\trecord(entry: PerformanceEntry): void {\n\t\tif (this.disposed) return;\n\t\ttry {\n\t\t\tconst wrote = this.withLogLock(() => {\n\t\t\t\tappendFileSync(this.logPath, `${JSON.stringify(entry)}\\n`, \"utf8\");\n\t\t\t\treturn true;\n\t\t\t});\n\t\t\tif (!wrote) {\n\t\t\t\tconsole.warn(\"[PerformanceTracker] Failed to write performance entry: could not acquire log lock\");\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.entries.push(entry);\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[PerformanceTracker] Failed to write performance entry: ${error}`);\n\t\t}\n\t}\n\n\tgetRollingAverage(provider: string, modelId: string, count = 100): RollingAverage {\n\t\tconst values = this.entries\n\t\t\t.filter((e) => e.provider === provider && e.modelId === modelId)\n\t\t\t.sort((a, b) => entryTime(b) - entryTime(a))\n\t\t\t.slice(0, count)\n\t\t\t.map((e) => e.tps);\n\n\t\tif (values.length === 0) {\n\t\t\treturn { median: 0, mean: 0, count: 0 };\n\t\t}\n\n\t\treturn {\n\t\t\tmedian: computeMedian(values),\n\t\t\tmean: computeMean(values),\n\t\t\tcount: values.length,\n\t\t};\n\t}\n\n\tgetPerformanceDelta(\n\t\tprovider: string,\n\t\tmodelId: string,\n\t\trecentCount = 10,\n\t\tbaselineCount = 10_000,\n\t\tstablePercent = 1,\n\t): PerformanceDelta {\n\t\tconst modelEntries = this.entries\n\t\t\t.filter((e) => e.provider === provider && e.modelId === modelId)\n\t\t\t.sort((a, b) => entryTime(b) - entryTime(a));\n\n\t\tconst baselineSlice = baselineCount > 0 ? baselineCount : modelEntries.length;\n\t\tconst baselineValues = modelEntries.slice(0, baselineSlice).map((e) => e.tps);\n\t\tconst recentValues = modelEntries.slice(0, recentCount).map((e) => e.tps);\n\n\t\tconst baselineMedian = computeMedian(baselineValues);\n\t\tconst recentMedian = computeMedian(recentValues);\n\t\tif (baselineValues.length < 3 || recentValues.length < 3 || baselineMedian <= 0) {\n\t\t\treturn {\n\t\t\t\tbaselineMedian,\n\t\t\t\trecentMedian,\n\t\t\t\tpercentDelta: 0,\n\t\t\t\tdirection: \"stable\",\n\t\t\t\tbaselineCount: baselineValues.length,\n\t\t\t\trecentCount: recentValues.length,\n\t\t\t};\n\t\t}\n\n\t\tconst percentDelta = ((recentMedian - baselineMedian) / baselineMedian) * 100;\n\t\tconst direction = Math.abs(percentDelta) < stablePercent ? \"stable\" : percentDelta > 0 ? \"above\" : \"below\";\n\n\t\treturn {\n\t\t\tbaselineMedian,\n\t\t\trecentMedian,\n\t\t\tpercentDelta,\n\t\t\tdirection,\n\t\t\tbaselineCount: baselineValues.length,\n\t\t\trecentCount: recentValues.length,\n\t\t};\n\t}\n\n\tgetAllRollingAverages(\n\t\twindowMs = 24 * 60 * 60 * 1000,\n\t): Array<{ provider: string; modelId: string; median: number; mean: number; count: number }> {\n\t\tconst cutoff = Date.now() - windowMs;\n\t\tconst filtered = this.entries.filter((e) => entryTime(e) >= cutoff);\n\n\t\tconst groups = new Map<string, number[]>();\n\t\tfor (const entry of filtered) {\n\t\t\tconst key = `${entry.provider}\\0${entry.modelId}`;\n\t\t\tconst arr = groups.get(key) ?? [];\n\t\t\tarr.push(entry.tps);\n\t\t\tgroups.set(key, arr);\n\t\t}\n\n\t\tconst results: Array<{ provider: string; modelId: string; median: number; mean: number; count: number }> = [];\n\t\tfor (const [key, values] of groups) {\n\t\t\tconst [provider, modelId] = key.split(\"\\0\");\n\t\t\tresults.push({\n\t\t\t\tprovider,\n\t\t\t\tmodelId,\n\t\t\t\tmedian: computeMedian(values),\n\t\t\t\tmean: computeMean(values),\n\t\t\t\tcount: values.length,\n\t\t\t});\n\t\t}\n\n\t\treturn results;\n\t}\n\n\tprune(ageMs = 30 * 24 * 60 * 60 * 1000): void {\n\t\tif (this.disposed) return;\n\t\tlet tempPath: string | undefined;\n\t\ttry {\n\t\t\tconst pruned = this.withLogLock(() => {\n\t\t\t\tconst cutoff = Date.now() - ageMs;\n\t\t\t\tconst sourceEntries = this.readEntries();\n\t\t\t\tconst kept: PerformanceEntry[] = [];\n\t\t\t\tconst lines: string[] = [];\n\n\t\t\t\tfor (const entry of sourceEntries) {\n\t\t\t\t\tif (entryTime(entry) >= cutoff) {\n\t\t\t\t\t\tkept.push(entry);\n\t\t\t\t\t\tlines.push(JSON.stringify(entry));\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\ttempPath = join(dirname(this.logPath), `.performance-prune-${process.pid}-${Date.now()}.jsonl`);\n\t\t\t\twriteFileSync(tempPath, lines.length > 0 ? `${lines.join(\"\\n\")}\\n` : \"\", \"utf8\");\n\t\t\t\trenameSync(tempPath, this.logPath);\n\t\t\t\ttempPath = undefined;\n\t\t\t\tthis.entries = kept;\n\t\t\t\treturn true;\n\t\t\t}, PerformanceTracker.PRUNE_LOCK_TIMEOUT_MS);\n\t\t\tif (!pruned) {\n\t\t\t\tconsole.warn(\"[PerformanceTracker] Failed to prune performance log: could not acquire log lock\");\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.warn(`[PerformanceTracker] Failed to prune performance log: ${error}`);\n\t\t} finally {\n\t\t\tif (tempPath) {\n\t\t\t\ttry {\n\t\t\t\t\trmSync(tempPath, { force: true });\n\t\t\t\t} catch {\n\t\t\t\t\t// Best-effort cleanup only\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdispose(): void {\n\t\tthis.disposed = true;\n\t\tif (this.pruneTimer) {\n\t\t\tclearInterval(this.pruneTimer);\n\t\t\tthis.pruneTimer = null;\n\t\t}\n\t}\n\n\tprivate ensureDir(): void {\n\t\ttry {\n\t\t\tmkdirSync(dirname(this.logPath), { recursive: true });\n\t\t} catch (error) {\n\t\t\tif (isFileExistsError(error)) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconsole.warn(`[PerformanceTracker] Failed to create log directory: ${error}`);\n\t\t}\n\t}\n\n\tprivate schedulePrune(): void {\n\t\tif (this.disposed) return;\n\t\tthis.pruneTimer = setInterval(() => {\n\t\t\tthis.prune();\n\t\t}, PerformanceTracker.PRUNE_INTERVAL_MS);\n\t\tthis.pruneTimer?.unref?.();\n\t}\n\n\tprivate readEntries(): PerformanceEntry[] {\n\t\ttry {\n\t\t\tconst content = readFileSync(this.logPath, \"utf8\");\n\t\t\tconst entries: PerformanceEntry[] = [];\n\t\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\t\tif (!line.trim()) continue;\n\t\t\t\ttry {\n\t\t\t\t\tconst parsed = JSON.parse(line) as unknown;\n\t\t\t\t\tif (!isValidPerformanceEntry(parsed)) {\n\t\t\t\t\t\tconsole.warn(`[PerformanceTracker] Skipping invalid performance entry line: ${line}`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tconst time = entryTime(parsed);\n\t\t\t\t\tif (!Number.isFinite(time)) {\n\t\t\t\t\t\tconsole.warn(`[PerformanceTracker] Skipping entry with invalid timestamp: ${line}`);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\tentries.push(parsed);\n\t\t\t\t} catch {\n\t\t\t\t\t// Skip malformed lines\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn entries;\n\t\t} catch (error) {\n\t\t\tif (isENOENT(error)) {\n\t\t\t\treturn [];\n\t\t\t}\n\t\t\tthrow error;\n\t\t}\n\t}\n\n\tprivate withLogLock<T>(operation: () => T, timeoutMs = PerformanceTracker.LOCK_TIMEOUT_MS): T | undefined {\n\t\tconst fd = this.acquireLock(timeoutMs);\n\t\tif (fd === undefined) return undefined;\n\t\ttry {\n\t\t\treturn operation();\n\t\t} finally {\n\t\t\ttry {\n\t\t\t\tcloseSync(fd);\n\t\t\t} finally {\n\t\t\t\trmSync(this.lockPath, { force: true });\n\t\t\t}\n\t\t}\n\t}\n\n\tprivate acquireLock(timeoutMs: number): number | undefined {\n\t\tconst start = performance.now();\n\t\twhile (performance.now() - start <= timeoutMs) {\n\t\t\ttry {\n\t\t\t\treturn openSync(this.lockPath, \"wx\");\n\t\t\t} catch (error) {\n\t\t\t\tif (!isFileExistsError(error)) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tthis.removeStaleLock();\n\t\t\t\tsleepSync(10);\n\t\t\t}\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tprivate removeStaleLock(): void {\n\t\ttry {\n\t\t\tconst lockAgeMs = Date.now() - statSync(this.lockPath).mtimeMs;\n\t\t\tif (lockAgeMs > PerformanceTracker.STALE_LOCK_MS) {\n\t\t\t\trmSync(this.lockPath, { force: true });\n\t\t\t}\n\t\t} catch {\n\t\t\t// Lock disappeared between open attempts\n\t\t}\n\t}\n}\n\nfunction entryTime(entry: PerformanceEntry): number {\n\tconst time = new Date(entry.timestamp).getTime();\n\treturn Number.isFinite(time) ? time : NaN;\n}\n\nfunction computeMedian(values: number[]): number {\n\tif (values.length === 0) return 0;\n\tconst sorted = [...values].sort((a, b) => a - b);\n\tconst mid = Math.floor(sorted.length / 2);\n\tif (sorted.length % 2 === 1) {\n\t\treturn sorted[mid];\n\t}\n\treturn (sorted[mid - 1] + sorted[mid]) / 2;\n}\n\nfunction computeMean(values: number[]): number {\n\tif (values.length === 0) return 0;\n\treturn values.reduce((a, b) => a + b, 0) / values.length;\n}\n\nfunction isFileExistsError(error: unknown): boolean {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"EEXIST\";\n}\n\nfunction isENOENT(error: unknown): boolean {\n\treturn typeof error === \"object\" && error !== null && \"code\" in error && error.code === \"ENOENT\";\n}\n\nfunction isValidPerformanceEntry(entry: unknown): entry is PerformanceEntry {\n\tif (typeof entry !== \"object\" || entry === null) return false;\n\tconst e = entry as Record<string, unknown>;\n\treturn (\n\t\ttypeof e.timestamp === \"string\" &&\n\t\ttypeof e.provider === \"string\" &&\n\t\ttypeof e.modelId === \"string\" &&\n\t\ttypeof e.outputTokens === \"number\" &&\n\t\tNumber.isFinite(e.outputTokens) &&\n\t\ttypeof e.durationMs === \"number\" &&\n\t\tNumber.isFinite(e.durationMs) &&\n\t\ttypeof e.tps === \"number\" &&\n\t\tNumber.isFinite(e.tps)\n\t);\n}\n\nfunction sleepSync(ms: number): void {\n\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);\n}\n"]}
|
package/dist/core/sdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"AACA,OAAO,EAA4B,KAAK,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,KAAK,EAAW,KAAK,EAAE,MAAM,UAAU,CAAC;AAE/C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAEhD,OAAO,KAAK,EAAmB,oBAAoB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAEnG,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAGpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE3D,OAAO,EAAwB,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAExD,OAAO,EACN,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,cAAc,EACd,iBAAiB,EACjB,cAAc,EACd,cAAc,EACd,cAAc,EACd,YAAY,EACZ,mBAAmB,EACnB,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,QAAQ,EACR,QAAQ,EACR,mBAAmB,EACnB,0BAA0B,EAC1B,QAAQ,EACR,MAAM,EACN,qBAAqB,EACrB,aAAa,EACb,QAAQ,EACR,YAAY,EACZ,KAAK,IAAI,EAET,qBAAqB,EACrB,SAAS,EACT,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,yBAAyB;IACzC,4EAA4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,sDAAsD;IACtD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,oFAAoF;IACpF,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,oFAAoF;IACpF,aAAa,CAAC,EAAE,aAAa,CAAC;IAE9B,iEAAiE;IACjE,KAAK,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IACnB,4FAA4F;IAC5F,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B,gEAAgE;IAChE,YAAY,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;QAAC,aAAa,CAAC,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IAE3E,yNAAyN;IACzN,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;IACf,gEAAgE;IAChE,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC;IAE/B,oEAAoE;IACpE,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC,2DAA2D;IAC3D,cAAc,CAAC,EAAE,cAAc,CAAC;IAEhC,uEAAuE;IACvE,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,wEAAwE;IACxE,MAAM,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qCAAqC;AACrC,MAAM,WAAW,wBAAwB;IACxC,0BAA0B;IAC1B,OAAO,EAAE,YAAY,CAAC;IACtB,mEAAmE;IACnE,gBAAgB,EAAE,oBAAoB,CAAC;IACvC,wEAAwE;IACxE,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAID,YAAY,EACX,YAAY,EACZ,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,GACd,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AAC5D,YAAY,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACzC,YAAY,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAE7C,OAAO,EAEN,QAAQ,EACR,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,YAAY,EACZ,WAAW,EACX,aAAa,EACb,QAAQ,IAAI,eAAe,EAC3B,qBAAqB,EAErB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,cAAc,EACd,cAAc,EACd,eAAe,EACf,cAAc,EACd,cAAc,EACd,YAAY,EACZ,kBAAkB,EAElB,mBAAmB,EACnB,0BAA0B,EAC1B,qBAAqB,GACrB,CAAC;AAQF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,OAAO,CAAC,wBAAwB,CAAC,CAmPnH","sourcesContent":["import { join } from \"node:path\";\nimport { Agent, type AgentMessage, type ThinkingLevel } from \"@dreb/agent-core\";\nimport type { Message, Model } from \"@dreb/ai\";\nimport { getAgentDir, getDocsPath } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, ToolDefinition } from \"./extensions/index.js\";\nimport { convertToLlm } from \"./messages.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { findInitialModel } from \"./model-resolver.js\";\nimport { configValueWarnings } from \"./resolve-config-value.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tallTools,\n\tbashTool,\n\tcodingTools,\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateSubagentTool,\n\tcreateWriteTool,\n\teditTool,\n\tfindTool,\n\tgetBackgroundAgents,\n\tgetRunningBackgroundAgents,\n\tgrepTool,\n\tlsTool,\n\tpruneBackgroundAgents,\n\treadOnlyTools,\n\treadTool,\n\tsubagentTool,\n\ttype Tool,\n\ttype ToolName,\n\twithFileMutationQueue,\n\twriteTool,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.dreb/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: new ModelRegistry(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/** Built-in tools to use. Default: all standard tools [read, bash, edit, write, grep, find, ls, web_search, web_fetch, subagent]. `skill`, `tasks_update`, and `search` are always active regardless of this setting. */\n\ttools?: Tool[];\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** UI type for system prompt context (e.g. \"tui\", \"telegram\", \"rpc\") */\n\tuiType?: string;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\t// Pre-built tools (use process.cwd())\n\treadTool,\n\tbashTool,\n\teditTool,\n\twriteTool,\n\tgrepTool,\n\tfindTool,\n\tlsTool,\n\tsubagentTool,\n\tcodingTools,\n\treadOnlyTools,\n\tallTools as allBuiltInTools,\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n\tcreateSubagentTool,\n\t// Background agent registry\n\tgetBackgroundAgents,\n\tgetRunningBackgroundAgents,\n\tpruneBackgroundAgents,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@dreb/ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tconst hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;\n\t\tif (restoredModel && hasApiKey) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tconst reason = !restoredModel ? \"not found in registry\" : \"no API key available\";\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId} (${reason})`;\n\t\t\tconsole.warn(`[model-restore] ${modelFallbackMessage}`);\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `No models available. Use /login or set an API key environment variable. See ${join(getDocsPath(), \"providers.md\")}. Then use /model to select a model.`;\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model || !model.reasoning) {\n\t\tthinkingLevel = \"off\";\n\t}\n\n\t// Tools that are always active when available (created by factory, not in allTools singleton)\n\tconst alwaysActiveBuiltins = [\"skill\", \"tasks_update\", \"search\"];\n\tconst defaultActiveToolNames: ToolName[] = [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t];\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools.map((t) => t.name).filter((n): n is ToolName => n in allTools), ...alwaysActiveBuiltins]\n\t\t: [...defaultActiveToolNames, ...alwaysActiveBuiltins];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\tconst sessionRef: { current?: AgentSession } = {};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner) return messages;\n\t\t\treturn runner.emitContext(messages);\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tmaxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,\n\t\tonWarning: (code: string, message: string) => {\n\t\t\t// Wire provider-level warnings to the session for user/agent visibility\n\t\t\tconst informational =\n\t\t\t\tcode === \"sse_parse_error\" || code === \"ws_parse_error\" || code === \"json_parse_total_failure\";\n\t\t\tsessionRef.current?.warnInSession(message, { informational });\n\t\t},\n\t\tgetApiKey: async (provider) => {\n\t\t\t// Use the provider argument from the in-flight request;\n\t\t\t// agent.state.model may already be switched mid-turn.\n\t\t\tconst resolvedProvider = provider || agent.state.model?.provider;\n\t\t\tif (!resolvedProvider) {\n\t\t\t\tthrow new Error(\"No model selected\");\n\t\t\t}\n\t\t\tconst key = await modelRegistry.getApiKeyForProvider(resolvedProvider);\n\t\t\t// Surface any config value resolution warnings (e.g. failed !command API keys)\n\t\t\tif (configValueWarnings.length > 0) {\n\t\t\t\tconst warnings = configValueWarnings.splice(0);\n\t\t\t\tfor (const w of warnings) {\n\t\t\t\t\tsessionRef.current?.warnInSession(w);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!key) {\n\t\t\t\tconst model = agent.state.model;\n\t\t\t\tconst isOAuth = model && modelRegistry.isUsingOAuth(model);\n\t\t\t\tif (isOAuth) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Authentication failed for \"${resolvedProvider}\". ` +\n\t\t\t\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t\t\t\t`Run '/login ${resolvedProvider}' to re-authenticate.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No API key found for \"${resolvedProvider}\". ` +\n\t\t\t\t\t\t`Set an API key environment variable or run '/login ${resolvedProvider}'.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn key;\n\t\t},\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.replaceMessages(existingSession.messages);\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\textensionRunnerRef,\n\t\tuiType: options.uiType,\n\t});\n\tsessionRef.current = session;\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\t// Surface any resource diagnostics from initial load\n\tsession.warnResourceDiagnostics(resourceLoader);\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/core/sdk.ts"],"names":[],"mappings":"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","sourcesContent":["import { join } from \"node:path\";\nimport { Agent, type AgentMessage, type ThinkingLevel } from \"@dreb/agent-core\";\nimport type { Message, Model } from \"@dreb/ai\";\nimport { getAgentDir, getDocsPath } from \"../config.js\";\nimport { AgentSession } from \"./agent-session.js\";\nimport { AuthStorage } from \"./auth-storage.js\";\nimport { DEFAULT_THINKING_LEVEL } from \"./defaults.js\";\nimport type { ExtensionRunner, LoadExtensionsResult, ToolDefinition } from \"./extensions/index.js\";\nimport { convertToLlm } from \"./messages.js\";\nimport { ModelRegistry } from \"./model-registry.js\";\nimport { findInitialModel } from \"./model-resolver.js\";\nimport { configValueWarnings } from \"./resolve-config-value.js\";\nimport type { ResourceLoader } from \"./resource-loader.js\";\nimport { DefaultResourceLoader } from \"./resource-loader.js\";\nimport { getDefaultSessionDir, SessionManager } from \"./session-manager.js\";\nimport { SettingsManager } from \"./settings-manager.js\";\nimport { time } from \"./timings.js\";\nimport {\n\tallTools,\n\tbashTool,\n\tcodingTools,\n\tcreateBashTool,\n\tcreateCodingTools,\n\tcreateEditTool,\n\tcreateFindTool,\n\tcreateGrepTool,\n\tcreateLsTool,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateSubagentTool,\n\tcreateWriteTool,\n\teditTool,\n\tfindTool,\n\tgetBackgroundAgents,\n\tgetRunningBackgroundAgents,\n\tgrepTool,\n\tlsTool,\n\tpruneBackgroundAgents,\n\treadOnlyTools,\n\treadTool,\n\tsubagentTool,\n\ttype Tool,\n\ttype ToolName,\n\twithFileMutationQueue,\n\twriteTool,\n} from \"./tools/index.js\";\n\nexport interface CreateAgentSessionOptions {\n\t/** Working directory for project-local discovery. Default: process.cwd() */\n\tcwd?: string;\n\t/** Global config directory. Default: ~/.dreb/agent */\n\tagentDir?: string;\n\n\t/** Auth storage for credentials. Default: AuthStorage.create(agentDir/auth.json) */\n\tauthStorage?: AuthStorage;\n\t/** Model registry. Default: new ModelRegistry(authStorage, agentDir/models.json) */\n\tmodelRegistry?: ModelRegistry;\n\n\t/** Model to use. Default: from settings, else first available */\n\tmodel?: Model<any>;\n\t/** Thinking level. Default: from settings, else 'medium' (clamped to model capabilities) */\n\tthinkingLevel?: ThinkingLevel;\n\t/** Models available for cycling (Ctrl+P in interactive mode) */\n\tscopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;\n\n\t/** Built-in tools to use. Default: all standard tools [read, bash, edit, write, grep, find, ls, web_search, web_fetch, subagent]. `skill`, `tasks_update`, and `search` are always active regardless of this setting. */\n\ttools?: Tool[];\n\t/** Custom tools to register (in addition to built-in tools). */\n\tcustomTools?: ToolDefinition[];\n\n\t/** Resource loader. When omitted, DefaultResourceLoader is used. */\n\tresourceLoader?: ResourceLoader;\n\n\t/** Session manager. Default: SessionManager.create(cwd) */\n\tsessionManager?: SessionManager;\n\n\t/** Settings manager. Default: SettingsManager.create(cwd, agentDir) */\n\tsettingsManager?: SettingsManager;\n\t/** UI type for system prompt context (e.g. \"tui\", \"telegram\", \"rpc\") */\n\tuiType?: string;\n}\n\n/** Result from createAgentSession */\nexport interface CreateAgentSessionResult {\n\t/** The created session */\n\tsession: AgentSession;\n\t/** Extensions result (for UI context setup in interactive mode) */\n\textensionsResult: LoadExtensionsResult;\n\t/** Warning if session was restored with a different model than saved */\n\tmodelFallbackMessage?: string;\n}\n\n// Re-exports\n\nexport type {\n\tExtensionAPI,\n\tExtensionCommandContext,\n\tExtensionContext,\n\tExtensionFactory,\n\tSlashCommandInfo,\n\tSlashCommandSource,\n\tToolDefinition,\n} from \"./extensions/index.js\";\nexport type { PromptTemplate } from \"./prompt-templates.js\";\nexport type { Skill } from \"./skills.js\";\nexport type { Tool } from \"./tools/index.js\";\n\nexport {\n\t// Pre-built tools (use process.cwd())\n\treadTool,\n\tbashTool,\n\teditTool,\n\twriteTool,\n\tgrepTool,\n\tfindTool,\n\tlsTool,\n\tsubagentTool,\n\tcodingTools,\n\treadOnlyTools,\n\tallTools as allBuiltInTools,\n\twithFileMutationQueue,\n\t// Tool factories (for custom cwd)\n\tcreateCodingTools,\n\tcreateReadOnlyTools,\n\tcreateReadTool,\n\tcreateBashTool,\n\tcreateEditTool,\n\tcreateWriteTool,\n\tcreateGrepTool,\n\tcreateFindTool,\n\tcreateLsTool,\n\tcreateSubagentTool,\n\t// Background agent registry\n\tgetBackgroundAgents,\n\tgetRunningBackgroundAgents,\n\tpruneBackgroundAgents,\n};\n\n// Helper Functions\n\nfunction getDefaultAgentDir(): string {\n\treturn getAgentDir();\n}\n\n/**\n * Create an AgentSession with the specified options.\n *\n * @example\n * ```typescript\n * // Minimal - uses defaults\n * const { session } = await createAgentSession();\n *\n * // With explicit model\n * import { getModel } from '@dreb/ai';\n * const { session } = await createAgentSession({\n * model: getModel('anthropic', 'claude-opus-4-5'),\n * thinkingLevel: 'high',\n * });\n *\n * // Continue previous session\n * const { session, modelFallbackMessage } = await createAgentSession({\n * continueSession: true,\n * });\n *\n * // Full control\n * const loader = new DefaultResourceLoader({\n * cwd: process.cwd(),\n * agentDir: getAgentDir(),\n * settingsManager: SettingsManager.create(),\n * });\n * await loader.reload();\n * const { session } = await createAgentSession({\n * model: myModel,\n * tools: [readTool, bashTool],\n * resourceLoader: loader,\n * sessionManager: SessionManager.inMemory(),\n * });\n * ```\n */\nexport async function createAgentSession(options: CreateAgentSessionOptions = {}): Promise<CreateAgentSessionResult> {\n\tconst cwd = options.cwd ?? process.cwd();\n\tconst agentDir = options.agentDir ?? getDefaultAgentDir();\n\tlet resourceLoader = options.resourceLoader;\n\n\t// Use provided or create AuthStorage and ModelRegistry\n\tconst authPath = options.agentDir ? join(agentDir, \"auth.json\") : undefined;\n\tconst modelsPath = options.agentDir ? join(agentDir, \"models.json\") : undefined;\n\tconst authStorage = options.authStorage ?? AuthStorage.create(authPath);\n\tconst modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage, modelsPath);\n\n\tconst settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);\n\tconst sessionManager = options.sessionManager ?? SessionManager.create(cwd, getDefaultSessionDir(cwd, agentDir));\n\n\tif (!resourceLoader) {\n\t\tresourceLoader = new DefaultResourceLoader({ cwd, agentDir, settingsManager });\n\t\tawait resourceLoader.reload();\n\t\ttime(\"resourceLoader.reload\");\n\t}\n\n\t// Check if session has existing data to restore\n\tconst existingSession = sessionManager.buildSessionContext();\n\tconst hasExistingSession = existingSession.messages.length > 0;\n\tconst hasThinkingEntry = sessionManager.getBranch().some((entry) => entry.type === \"thinking_level_change\");\n\n\tlet model = options.model;\n\tlet modelFallbackMessage: string | undefined;\n\n\t// If session has data, try to restore model from it\n\tif (!model && hasExistingSession && existingSession.model) {\n\t\tconst restoredModel = modelRegistry.find(existingSession.model.provider, existingSession.model.modelId);\n\t\tconst hasApiKey = restoredModel ? !!(await modelRegistry.getApiKey(restoredModel)) : false;\n\t\tif (restoredModel && hasApiKey) {\n\t\t\tmodel = restoredModel;\n\t\t}\n\t\tif (!model) {\n\t\t\tconst reason = !restoredModel ? \"not found in registry\" : \"no API key available\";\n\t\t\tmodelFallbackMessage = `Could not restore model ${existingSession.model.provider}/${existingSession.model.modelId} (${reason})`;\n\t\t\tconsole.warn(`[model-restore] ${modelFallbackMessage}`);\n\t\t}\n\t}\n\n\t// If still no model, use findInitialModel (checks settings default, then provider defaults)\n\tif (!model) {\n\t\tconst result = await findInitialModel({\n\t\t\tscopedModels: [],\n\t\t\tisContinuing: hasExistingSession,\n\t\t\tdefaultProvider: settingsManager.getDefaultProvider(),\n\t\t\tdefaultModelId: settingsManager.getDefaultModel(),\n\t\t\tdefaultThinkingLevel: settingsManager.getDefaultThinkingLevel(),\n\t\t\tmodelRegistry,\n\t\t});\n\t\tmodel = result.model;\n\t\tif (!model) {\n\t\t\tmodelFallbackMessage = `No models available. Use /login or set an API key environment variable. See ${join(getDocsPath(), \"providers.md\")}. Then use /model to select a model.`;\n\t\t} else if (modelFallbackMessage) {\n\t\t\tmodelFallbackMessage += `. Using ${model.provider}/${model.id}`;\n\t\t}\n\t}\n\n\tlet thinkingLevel = options.thinkingLevel;\n\n\t// If session has data, restore thinking level from it\n\tif (thinkingLevel === undefined && hasExistingSession) {\n\t\tthinkingLevel = hasThinkingEntry\n\t\t\t? (existingSession.thinkingLevel as ThinkingLevel)\n\t\t\t: (settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL);\n\t}\n\n\t// Fall back to settings default\n\tif (thinkingLevel === undefined) {\n\t\tthinkingLevel = settingsManager.getDefaultThinkingLevel() ?? DEFAULT_THINKING_LEVEL;\n\t}\n\n\t// Clamp to model capabilities\n\tif (!model || !model.reasoning) {\n\t\tthinkingLevel = \"off\";\n\t}\n\n\t// Tools that are always active when available (created by factory, not in allTools singleton)\n\tconst alwaysActiveBuiltins = [\"skill\", \"tasks_update\", \"search\", \"suggest_next\"];\n\tconst defaultActiveToolNames: ToolName[] = [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"grep\",\n\t\t\"find\",\n\t\t\"ls\",\n\t\t\"web_search\",\n\t\t\"web_fetch\",\n\t\t\"subagent\",\n\t];\n\tconst initialActiveToolNames: string[] = options.tools\n\t\t? [...options.tools.map((t) => t.name).filter((n): n is ToolName => n in allTools), ...alwaysActiveBuiltins]\n\t\t: [...defaultActiveToolNames, ...alwaysActiveBuiltins];\n\n\tlet agent: Agent;\n\n\t// Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)\n\tconst convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {\n\t\tconst converted = convertToLlm(messages);\n\t\t// Check setting dynamically so mid-session changes take effect\n\t\tif (!settingsManager.getBlockImages()) {\n\t\t\treturn converted;\n\t\t}\n\t\t// Filter out ImageContent from all messages, replacing with text placeholder\n\t\treturn converted.map((msg) => {\n\t\t\tif (msg.role === \"user\" || msg.role === \"toolResult\") {\n\t\t\t\tconst content = msg.content;\n\t\t\t\tif (Array.isArray(content)) {\n\t\t\t\t\tconst hasImages = content.some((c) => c.type === \"image\");\n\t\t\t\t\tif (hasImages) {\n\t\t\t\t\t\tconst filteredContent = content\n\t\t\t\t\t\t\t.map((c) =>\n\t\t\t\t\t\t\t\tc.type === \"image\" ? { type: \"text\" as const, text: \"Image reading is disabled.\" } : c,\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t.filter(\n\t\t\t\t\t\t\t\t(c, i, arr) =>\n\t\t\t\t\t\t\t\t\t// Dedupe consecutive \"Image reading is disabled.\" texts\n\t\t\t\t\t\t\t\t\t!(\n\t\t\t\t\t\t\t\t\t\tc.type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\tc.text === \"Image reading is disabled.\" &&\n\t\t\t\t\t\t\t\t\t\ti > 0 &&\n\t\t\t\t\t\t\t\t\t\tarr[i - 1].type === \"text\" &&\n\t\t\t\t\t\t\t\t\t\t(arr[i - 1] as { type: \"text\"; text: string }).text === \"Image reading is disabled.\"\n\t\t\t\t\t\t\t\t\t),\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\treturn { ...msg, content: filteredContent };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn msg;\n\t\t});\n\t};\n\n\tconst extensionRunnerRef: { current?: ExtensionRunner } = {};\n\tconst sessionRef: { current?: AgentSession } = {};\n\n\tagent = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: \"\",\n\t\t\tmodel,\n\t\t\tthinkingLevel,\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm: convertToLlmWithBlockImages,\n\t\tonPayload: async (payload, _model) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner?.hasHandlers(\"before_provider_request\")) {\n\t\t\t\treturn payload;\n\t\t\t}\n\t\t\treturn runner.emitBeforeProviderRequest(payload);\n\t\t},\n\t\tsessionId: sessionManager.getSessionId(),\n\t\ttransformContext: async (messages) => {\n\t\t\tconst runner = extensionRunnerRef.current;\n\t\t\tif (!runner) return messages;\n\t\t\treturn runner.emitContext(messages);\n\t\t},\n\t\tsteeringMode: settingsManager.getSteeringMode(),\n\t\tfollowUpMode: settingsManager.getFollowUpMode(),\n\t\ttransport: settingsManager.getTransport(),\n\t\tthinkingBudgets: settingsManager.getThinkingBudgets(),\n\t\tmaxRetryDelayMs: settingsManager.getRetrySettings().maxDelayMs,\n\t\tonWarning: (code: string, message: string) => {\n\t\t\t// Wire provider-level warnings to the session for user/agent visibility\n\t\t\tconst informational =\n\t\t\t\tcode === \"sse_parse_error\" || code === \"ws_parse_error\" || code === \"json_parse_total_failure\";\n\t\t\tsessionRef.current?.warnInSession(message, { informational });\n\t\t},\n\t\tgetApiKey: async (provider) => {\n\t\t\t// Use the provider argument from the in-flight request;\n\t\t\t// agent.state.model may already be switched mid-turn.\n\t\t\tconst resolvedProvider = provider || agent.state.model?.provider;\n\t\t\tif (!resolvedProvider) {\n\t\t\t\tthrow new Error(\"No model selected\");\n\t\t\t}\n\t\t\tconst key = await modelRegistry.getApiKeyForProvider(resolvedProvider);\n\t\t\t// Surface any config value resolution warnings (e.g. failed !command API keys)\n\t\t\tif (configValueWarnings.length > 0) {\n\t\t\t\tconst warnings = configValueWarnings.splice(0);\n\t\t\t\tfor (const w of warnings) {\n\t\t\t\t\tsessionRef.current?.warnInSession(w);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (!key) {\n\t\t\t\tconst model = agent.state.model;\n\t\t\t\tconst isOAuth = model && modelRegistry.isUsingOAuth(model);\n\t\t\t\tif (isOAuth) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`Authentication failed for \"${resolvedProvider}\". ` +\n\t\t\t\t\t\t\t`Credentials may have expired or network is unavailable. ` +\n\t\t\t\t\t\t\t`Run '/login ${resolvedProvider}' to re-authenticate.`,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`No API key found for \"${resolvedProvider}\". ` +\n\t\t\t\t\t\t`Set an API key environment variable or run '/login ${resolvedProvider}'.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn key;\n\t\t},\n\t});\n\n\t// Restore messages if session has existing data\n\tif (hasExistingSession) {\n\t\tagent.replaceMessages(existingSession.messages);\n\t\tif (!hasThinkingEntry) {\n\t\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t\t}\n\t} else {\n\t\t// Save initial model and thinking level for new sessions so they can be restored on resume\n\t\tif (model) {\n\t\t\tsessionManager.appendModelChange(model.provider, model.id);\n\t\t}\n\t\tsessionManager.appendThinkingLevelChange(thinkingLevel);\n\t}\n\n\tconst session = new AgentSession({\n\t\tagent,\n\t\tsessionManager,\n\t\tsettingsManager,\n\t\tcwd,\n\t\tscopedModels: options.scopedModels,\n\t\tresourceLoader,\n\t\tcustomTools: options.customTools,\n\t\tmodelRegistry,\n\t\tinitialActiveToolNames,\n\t\textensionRunnerRef,\n\t\tuiType: options.uiType,\n\t});\n\tsessionRef.current = session;\n\tconst extensionsResult = resourceLoader.getExtensions();\n\n\t// Surface any resource diagnostics from initial load\n\tsession.warnResourceDiagnostics(resourceLoader);\n\n\treturn {\n\t\tsession,\n\t\textensionsResult,\n\t\tmodelFallbackMessage,\n\t};\n}\n"]}
|
package/dist/core/sdk.js
CHANGED
|
@@ -128,7 +128,7 @@ export async function createAgentSession(options = {}) {
|
|
|
128
128
|
thinkingLevel = "off";
|
|
129
129
|
}
|
|
130
130
|
// Tools that are always active when available (created by factory, not in allTools singleton)
|
|
131
|
-
const alwaysActiveBuiltins = ["skill", "tasks_update", "search"];
|
|
131
|
+
const alwaysActiveBuiltins = ["skill", "tasks_update", "search", "suggest_next"];
|
|
132
132
|
const defaultActiveToolNames = [
|
|
133
133
|
"read",
|
|
134
134
|
"bash",
|