@yuaone/core 0.1.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/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module perf-optimizer
|
|
3
|
+
* @description YUAN Performance Optimizer — monitors and optimizes agent execution performance.
|
|
4
|
+
*
|
|
5
|
+
* Provides:
|
|
6
|
+
* - Execution timing per phase and tool call
|
|
7
|
+
* - Token budget tracking and waste detection
|
|
8
|
+
* - Parallelization hint generation from task dependency graphs
|
|
9
|
+
* - Content-addressable caching for tool calls and LLM responses
|
|
10
|
+
* - Human-readable performance reports with efficiency scoring
|
|
11
|
+
*
|
|
12
|
+
* Only depends on Node builtins (crypto for cache keys).
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
16
|
+
// Constants
|
|
17
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
18
|
+
const DEFAULT_MAX_CACHE_SIZE = 500;
|
|
19
|
+
const DEFAULT_MAX_CACHE_MEMORY = 50 * 1024 * 1024; // 50 MB
|
|
20
|
+
const DEFAULT_BOTTLENECK_THRESHOLD = 40; // percent
|
|
21
|
+
const MAX_METRICS = 10_000;
|
|
22
|
+
const MAX_TOOL_CALL_RECORDS = 5_000;
|
|
23
|
+
const MAX_SUGGESTIONS = 50;
|
|
24
|
+
const MAX_BOTTLENECKS = 50;
|
|
25
|
+
const MAX_PARALLEL_HINTS = 100;
|
|
26
|
+
/** Estimated average ms saved per cache hit (for reporting) */
|
|
27
|
+
const AVG_CACHE_HIT_SAVING_MS = 200;
|
|
28
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
29
|
+
// PerfOptimizer
|
|
30
|
+
// ══════════════════════════════════════════════════════════════════════
|
|
31
|
+
/**
|
|
32
|
+
* Monitors and optimizes agent execution performance.
|
|
33
|
+
*
|
|
34
|
+
* Tracks phase timing, token usage, tool call durations, and caching.
|
|
35
|
+
* Generates actionable performance reports with bottleneck identification,
|
|
36
|
+
* parallelization hints, and an overall efficiency score.
|
|
37
|
+
*/
|
|
38
|
+
export class PerfOptimizer {
|
|
39
|
+
config;
|
|
40
|
+
// Phase tracking
|
|
41
|
+
activePhases = new Map();
|
|
42
|
+
completedPhases = [];
|
|
43
|
+
// Tool call tracking
|
|
44
|
+
toolCallRecords = [];
|
|
45
|
+
// Metrics
|
|
46
|
+
metrics = [];
|
|
47
|
+
// Cache
|
|
48
|
+
cache = new Map();
|
|
49
|
+
totalCacheMemory = 0;
|
|
50
|
+
globalCacheHits = 0;
|
|
51
|
+
globalCacheMisses = 0;
|
|
52
|
+
// Historical data for comparison
|
|
53
|
+
previousRunDurations = [];
|
|
54
|
+
constructor(config) {
|
|
55
|
+
this.config = {
|
|
56
|
+
enableCaching: config?.enableCaching ?? true,
|
|
57
|
+
maxCacheSize: config?.maxCacheSize ?? DEFAULT_MAX_CACHE_SIZE,
|
|
58
|
+
maxCacheMemory: config?.maxCacheMemory ?? DEFAULT_MAX_CACHE_MEMORY,
|
|
59
|
+
trackParallelHints: config?.trackParallelHints ?? true,
|
|
60
|
+
bottleneckThresholdPercent: config?.bottleneckThresholdPercent ?? DEFAULT_BOTTLENECK_THRESHOLD,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// ────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Tracking
|
|
65
|
+
// ────────────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Mark the start of an execution phase.
|
|
68
|
+
* @param phase - Phase name (e.g. "analyze", "plan", "implement", "verify", "fix")
|
|
69
|
+
*/
|
|
70
|
+
startPhase(phase) {
|
|
71
|
+
this.activePhases.set(phase, {
|
|
72
|
+
phase,
|
|
73
|
+
startTime: Date.now(),
|
|
74
|
+
tokens: { input: 0, output: 0 },
|
|
75
|
+
toolCalls: 0,
|
|
76
|
+
cacheHits: 0,
|
|
77
|
+
cacheMisses: 0,
|
|
78
|
+
});
|
|
79
|
+
this.addMetric({
|
|
80
|
+
name: `phase.${phase}.start`,
|
|
81
|
+
category: "timing",
|
|
82
|
+
value: Date.now(),
|
|
83
|
+
unit: "epoch_ms",
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Mark the end of an execution phase.
|
|
89
|
+
* @param phase - Phase name that was previously started
|
|
90
|
+
* @param tokens - Optional token usage for this phase
|
|
91
|
+
*/
|
|
92
|
+
endPhase(phase, tokens) {
|
|
93
|
+
const active = this.activePhases.get(phase);
|
|
94
|
+
if (!active)
|
|
95
|
+
return;
|
|
96
|
+
const endTime = Date.now();
|
|
97
|
+
const durationMs = endTime - active.startTime;
|
|
98
|
+
if (tokens) {
|
|
99
|
+
active.tokens.input += tokens.input;
|
|
100
|
+
active.tokens.output += tokens.output;
|
|
101
|
+
}
|
|
102
|
+
const completed = {
|
|
103
|
+
phase: active.phase,
|
|
104
|
+
startTime: active.startTime,
|
|
105
|
+
endTime,
|
|
106
|
+
durationMs,
|
|
107
|
+
tokensUsed: { ...active.tokens },
|
|
108
|
+
toolCalls: active.toolCalls,
|
|
109
|
+
cacheHits: active.cacheHits,
|
|
110
|
+
cacheMisses: active.cacheMisses,
|
|
111
|
+
};
|
|
112
|
+
this.completedPhases.push(completed);
|
|
113
|
+
this.activePhases.delete(phase);
|
|
114
|
+
this.addMetric({
|
|
115
|
+
name: `phase.${phase}.duration`,
|
|
116
|
+
category: "timing",
|
|
117
|
+
value: durationMs,
|
|
118
|
+
unit: "ms",
|
|
119
|
+
timestamp: endTime,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Record a tool call with its duration.
|
|
124
|
+
* Automatically tracks cache hits/misses for the active phase.
|
|
125
|
+
* @param tool - Tool name
|
|
126
|
+
* @param input - Tool input (used for cache key generation)
|
|
127
|
+
* @param durationMs - How long the tool call took
|
|
128
|
+
*/
|
|
129
|
+
recordToolCall(tool, input, durationMs) {
|
|
130
|
+
const inputHash = this.hashInput(tool, input);
|
|
131
|
+
const cached = this.cache.has(inputHash);
|
|
132
|
+
if (this.toolCallRecords.length < MAX_TOOL_CALL_RECORDS) {
|
|
133
|
+
this.toolCallRecords.push({
|
|
134
|
+
tool,
|
|
135
|
+
inputHash,
|
|
136
|
+
durationMs,
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
cached,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
// Update active phase counters
|
|
142
|
+
for (const active of this.activePhases.values()) {
|
|
143
|
+
active.toolCalls++;
|
|
144
|
+
if (cached) {
|
|
145
|
+
active.cacheHits++;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
active.cacheMisses++;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.addMetric({
|
|
152
|
+
name: `tool.${tool}.duration`,
|
|
153
|
+
category: "timing",
|
|
154
|
+
value: durationMs,
|
|
155
|
+
unit: "ms",
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Record token usage for a specific phase.
|
|
161
|
+
* @param phase - Phase name
|
|
162
|
+
* @param input - Input tokens consumed
|
|
163
|
+
* @param output - Output tokens generated
|
|
164
|
+
*/
|
|
165
|
+
recordTokenUsage(phase, input, output) {
|
|
166
|
+
const active = this.activePhases.get(phase);
|
|
167
|
+
if (active) {
|
|
168
|
+
active.tokens.input += input;
|
|
169
|
+
active.tokens.output += output;
|
|
170
|
+
}
|
|
171
|
+
// Also update completed phase if it exists (for late-arriving token counts)
|
|
172
|
+
const completed = this.completedPhases.find((p) => p.phase === phase);
|
|
173
|
+
if (completed) {
|
|
174
|
+
completed.tokensUsed.input += input;
|
|
175
|
+
completed.tokensUsed.output += output;
|
|
176
|
+
}
|
|
177
|
+
this.addMetric({
|
|
178
|
+
name: `tokens.${phase}`,
|
|
179
|
+
category: "tokens",
|
|
180
|
+
value: input + output,
|
|
181
|
+
unit: "tokens",
|
|
182
|
+
timestamp: Date.now(),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// ────────────────────────────────────────────────────────────────────
|
|
186
|
+
// Caching
|
|
187
|
+
// ────────────────────────────────────────────────────────────────────
|
|
188
|
+
/**
|
|
189
|
+
* Retrieve a cached tool call result.
|
|
190
|
+
* @param tool - Tool name
|
|
191
|
+
* @param input - Tool input
|
|
192
|
+
* @returns Cached result or undefined if not found
|
|
193
|
+
*/
|
|
194
|
+
getCached(tool, input) {
|
|
195
|
+
if (!this.config.enableCaching)
|
|
196
|
+
return undefined;
|
|
197
|
+
const key = this.hashInput(tool, input);
|
|
198
|
+
const entry = this.cache.get(key);
|
|
199
|
+
if (!entry) {
|
|
200
|
+
this.globalCacheMisses++;
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
entry.hits++;
|
|
204
|
+
entry.lastAccessedAt = Date.now();
|
|
205
|
+
this.globalCacheHits++;
|
|
206
|
+
return entry.value;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Store a tool call result in the cache.
|
|
210
|
+
* Automatically evicts LRU entries when limits are exceeded.
|
|
211
|
+
* @param tool - Tool name
|
|
212
|
+
* @param input - Tool input
|
|
213
|
+
* @param result - Result to cache
|
|
214
|
+
*/
|
|
215
|
+
setCached(tool, input, result) {
|
|
216
|
+
if (!this.config.enableCaching)
|
|
217
|
+
return;
|
|
218
|
+
const key = this.hashInput(tool, input);
|
|
219
|
+
const serialized = JSON.stringify(result);
|
|
220
|
+
const sizeBytes = Buffer.byteLength(serialized, "utf8");
|
|
221
|
+
// Don't cache if single entry exceeds half the memory limit
|
|
222
|
+
if (sizeBytes > this.config.maxCacheMemory / 2)
|
|
223
|
+
return;
|
|
224
|
+
// Evict if needed
|
|
225
|
+
this.evictIfNeeded(sizeBytes);
|
|
226
|
+
// Remove old entry if updating
|
|
227
|
+
const existing = this.cache.get(key);
|
|
228
|
+
if (existing) {
|
|
229
|
+
this.totalCacheMemory -= existing.sizeBytes;
|
|
230
|
+
}
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
this.cache.set(key, {
|
|
233
|
+
key,
|
|
234
|
+
value: result,
|
|
235
|
+
hits: 0,
|
|
236
|
+
createdAt: now,
|
|
237
|
+
lastAccessedAt: now,
|
|
238
|
+
sizeBytes,
|
|
239
|
+
});
|
|
240
|
+
this.totalCacheMemory += sizeBytes;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Clear all cached entries.
|
|
244
|
+
*/
|
|
245
|
+
clearCache() {
|
|
246
|
+
this.cache.clear();
|
|
247
|
+
this.totalCacheMemory = 0;
|
|
248
|
+
}
|
|
249
|
+
// ────────────────────────────────────────────────────────────────────
|
|
250
|
+
// Analysis
|
|
251
|
+
// ────────────────────────────────────────────────────────────────────
|
|
252
|
+
/**
|
|
253
|
+
* Identify execution bottlenecks based on phase timing and token usage.
|
|
254
|
+
* @returns Array of bottleneck descriptions with suggestions
|
|
255
|
+
*/
|
|
256
|
+
getBottlenecks() {
|
|
257
|
+
const bottlenecks = [];
|
|
258
|
+
const totalMs = this.getTotalDuration();
|
|
259
|
+
if (totalMs === 0)
|
|
260
|
+
return bottlenecks;
|
|
261
|
+
const threshold = this.config.bottleneckThresholdPercent;
|
|
262
|
+
for (const phase of this.completedPhases) {
|
|
263
|
+
const pct = (phase.durationMs / totalMs) * 100;
|
|
264
|
+
// Phase taking too long
|
|
265
|
+
if (pct > threshold) {
|
|
266
|
+
bottlenecks.push({
|
|
267
|
+
phase: phase.phase,
|
|
268
|
+
issue: `Phase "${phase.phase}" consumed ${pct.toFixed(1)}% of total time (${phase.durationMs}ms)`,
|
|
269
|
+
impact: pct > 60 ? "high" : "medium",
|
|
270
|
+
suggestion: `Consider breaking "${phase.phase}" into smaller sub-phases or parallelizing tool calls within it`,
|
|
271
|
+
estimatedSavingMs: Math.round(phase.durationMs * (1 - threshold / pct)),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
// High cache miss rate with many tool calls
|
|
275
|
+
const totalCacheOps = phase.cacheHits + phase.cacheMisses;
|
|
276
|
+
if (totalCacheOps > 5 &&
|
|
277
|
+
phase.cacheMisses / totalCacheOps > 0.8) {
|
|
278
|
+
bottlenecks.push({
|
|
279
|
+
phase: phase.phase,
|
|
280
|
+
issue: `Phase "${phase.phase}" has ${((phase.cacheMisses / totalCacheOps) * 100).toFixed(0)}% cache miss rate across ${totalCacheOps} operations`,
|
|
281
|
+
impact: "medium",
|
|
282
|
+
suggestion: "Enable or increase cache size to avoid repeated identical tool calls",
|
|
283
|
+
estimatedSavingMs: Math.round(phase.cacheMisses * AVG_CACHE_HIT_SAVING_MS * 0.5),
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
// Token waste: high output tokens relative to input
|
|
287
|
+
const totalTokens = phase.tokensUsed.input + phase.tokensUsed.output;
|
|
288
|
+
if (totalTokens > 10_000 &&
|
|
289
|
+
phase.tokensUsed.output > phase.tokensUsed.input * 3) {
|
|
290
|
+
bottlenecks.push({
|
|
291
|
+
phase: phase.phase,
|
|
292
|
+
issue: `Phase "${phase.phase}" has disproportionate output tokens (${phase.tokensUsed.output} out vs ${phase.tokensUsed.input} in)`,
|
|
293
|
+
impact: "low",
|
|
294
|
+
suggestion: "Consider requesting more concise LLM responses or summarizing intermediate outputs",
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
// Detect repeated tool calls (same tool + input)
|
|
299
|
+
const callCounts = new Map();
|
|
300
|
+
for (const rec of this.toolCallRecords) {
|
|
301
|
+
const count = (callCounts.get(rec.inputHash) ?? 0) + 1;
|
|
302
|
+
callCounts.set(rec.inputHash, count);
|
|
303
|
+
}
|
|
304
|
+
for (const [hash, count] of callCounts) {
|
|
305
|
+
if (count >= 3) {
|
|
306
|
+
const sample = this.toolCallRecords.find((r) => r.inputHash === hash);
|
|
307
|
+
if (sample) {
|
|
308
|
+
bottlenecks.push({
|
|
309
|
+
phase: "global",
|
|
310
|
+
issue: `Tool "${sample.tool}" called ${count} times with identical input`,
|
|
311
|
+
impact: count >= 5 ? "high" : "medium",
|
|
312
|
+
suggestion: `Cache results for "${sample.tool}" to avoid redundant calls`,
|
|
313
|
+
estimatedSavingMs: Math.round((count - 1) * sample.durationMs),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Historical comparison
|
|
319
|
+
if (this.previousRunDurations.length > 0) {
|
|
320
|
+
const avg = this.previousRunDurations.reduce((a, b) => a + b, 0) /
|
|
321
|
+
this.previousRunDurations.length;
|
|
322
|
+
if (totalMs > avg * 1.5) {
|
|
323
|
+
bottlenecks.push({
|
|
324
|
+
phase: "global",
|
|
325
|
+
issue: `This run (${totalMs}ms) is ${((totalMs / avg - 1) * 100).toFixed(0)}% slower than average (${Math.round(avg)}ms)`,
|
|
326
|
+
impact: totalMs > avg * 2 ? "high" : "medium",
|
|
327
|
+
suggestion: "Check for new bottlenecks or increased task complexity",
|
|
328
|
+
estimatedSavingMs: Math.round(totalMs - avg),
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return bottlenecks.slice(0, MAX_BOTTLENECKS);
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Analyze a task dependency graph and identify parallelization opportunities.
|
|
336
|
+
* @param taskDeps - Map of taskId → array of dependency taskIds
|
|
337
|
+
* @returns Array of parallelization hints with estimated speedup
|
|
338
|
+
*/
|
|
339
|
+
getParallelHints(taskDeps) {
|
|
340
|
+
if (!this.config.trackParallelHints)
|
|
341
|
+
return [];
|
|
342
|
+
const hints = [];
|
|
343
|
+
// Find groups of tasks with the same dependencies (can run in parallel)
|
|
344
|
+
const depSignatures = new Map();
|
|
345
|
+
for (const [taskId, deps] of taskDeps) {
|
|
346
|
+
const sig = [...deps].sort().join(",");
|
|
347
|
+
const group = depSignatures.get(sig) ?? [];
|
|
348
|
+
group.push(taskId);
|
|
349
|
+
depSignatures.set(sig, group);
|
|
350
|
+
}
|
|
351
|
+
// Build duration map from tool call records
|
|
352
|
+
const taskDurations = new Map();
|
|
353
|
+
for (const rec of this.toolCallRecords) {
|
|
354
|
+
// Use tool name as rough proxy for task duration
|
|
355
|
+
const current = taskDurations.get(rec.tool) ?? 0;
|
|
356
|
+
taskDurations.set(rec.tool, current + rec.durationMs);
|
|
357
|
+
}
|
|
358
|
+
for (const [_sig, group] of depSignatures) {
|
|
359
|
+
if (group.length < 2)
|
|
360
|
+
continue;
|
|
361
|
+
// Estimate durations: use recorded data or default 1000ms
|
|
362
|
+
const durations = group.map((id) => taskDurations.get(id) ?? 1000);
|
|
363
|
+
const sequentialMs = durations.reduce((a, b) => a + b, 0);
|
|
364
|
+
const parallelMs = Math.max(...durations);
|
|
365
|
+
const speedup = parallelMs > 0 ? sequentialMs / parallelMs : 1;
|
|
366
|
+
if (speedup > 1.2) {
|
|
367
|
+
hints.push({
|
|
368
|
+
taskIds: group,
|
|
369
|
+
currentSequentialMs: sequentialMs,
|
|
370
|
+
estimatedParallelMs: parallelMs,
|
|
371
|
+
speedupFactor: Math.round(speedup * 100) / 100,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Also check for independent tasks (no deps at all)
|
|
376
|
+
const noDeps = [];
|
|
377
|
+
for (const [taskId, deps] of taskDeps) {
|
|
378
|
+
if (deps.length === 0)
|
|
379
|
+
noDeps.push(taskId);
|
|
380
|
+
}
|
|
381
|
+
if (noDeps.length >= 2) {
|
|
382
|
+
const durations = noDeps.map((id) => taskDurations.get(id) ?? 1000);
|
|
383
|
+
const sequentialMs = durations.reduce((a, b) => a + b, 0);
|
|
384
|
+
const parallelMs = Math.max(...durations);
|
|
385
|
+
const speedup = parallelMs > 0 ? sequentialMs / parallelMs : 1;
|
|
386
|
+
if (speedup > 1.2) {
|
|
387
|
+
// Avoid duplicate if already added via signature grouping
|
|
388
|
+
const alreadyAdded = hints.some((h) => h.taskIds.length === noDeps.length &&
|
|
389
|
+
h.taskIds.every((id) => noDeps.includes(id)));
|
|
390
|
+
if (!alreadyAdded) {
|
|
391
|
+
hints.push({
|
|
392
|
+
taskIds: noDeps,
|
|
393
|
+
currentSequentialMs: sequentialMs,
|
|
394
|
+
estimatedParallelMs: parallelMs,
|
|
395
|
+
speedupFactor: Math.round(speedup * 100) / 100,
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return hints.slice(0, MAX_PARALLEL_HINTS);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Calculate an overall efficiency score (0-100) based on multiple factors.
|
|
404
|
+
* @returns Efficiency score where 100 is optimal
|
|
405
|
+
*/
|
|
406
|
+
getEfficiencyScore() {
|
|
407
|
+
let score = 100;
|
|
408
|
+
const totalMs = this.getTotalDuration();
|
|
409
|
+
if (totalMs === 0)
|
|
410
|
+
return score;
|
|
411
|
+
// Penalty: bottlenecks
|
|
412
|
+
const bottlenecks = this.getBottlenecks();
|
|
413
|
+
for (const b of bottlenecks) {
|
|
414
|
+
switch (b.impact) {
|
|
415
|
+
case "high":
|
|
416
|
+
score -= 15;
|
|
417
|
+
break;
|
|
418
|
+
case "medium":
|
|
419
|
+
score -= 8;
|
|
420
|
+
break;
|
|
421
|
+
case "low":
|
|
422
|
+
score -= 3;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
// Bonus: good cache hit rate
|
|
427
|
+
const totalCacheOps = this.globalCacheHits + this.globalCacheMisses;
|
|
428
|
+
if (totalCacheOps > 0) {
|
|
429
|
+
const hitRate = this.globalCacheHits / totalCacheOps;
|
|
430
|
+
if (hitRate > 0.5)
|
|
431
|
+
score += 5;
|
|
432
|
+
if (hitRate > 0.7)
|
|
433
|
+
score += 5;
|
|
434
|
+
}
|
|
435
|
+
// Penalty: token waste (repeated identical tool calls)
|
|
436
|
+
const duplicateToolCalls = this.countDuplicateToolCalls();
|
|
437
|
+
const totalToolCalls = this.toolCallRecords.length;
|
|
438
|
+
if (totalToolCalls > 0) {
|
|
439
|
+
const wasteRatio = duplicateToolCalls / totalToolCalls;
|
|
440
|
+
if (wasteRatio > 0.3)
|
|
441
|
+
score -= 10;
|
|
442
|
+
else if (wasteRatio > 0.1)
|
|
443
|
+
score -= 5;
|
|
444
|
+
}
|
|
445
|
+
// Penalty: slower than historical average
|
|
446
|
+
if (this.previousRunDurations.length > 0) {
|
|
447
|
+
const avg = this.previousRunDurations.reduce((a, b) => a + b, 0) /
|
|
448
|
+
this.previousRunDurations.length;
|
|
449
|
+
if (totalMs > avg * 2)
|
|
450
|
+
score -= 10;
|
|
451
|
+
else if (totalMs > avg * 1.5)
|
|
452
|
+
score -= 5;
|
|
453
|
+
}
|
|
454
|
+
return Math.max(0, Math.min(100, score));
|
|
455
|
+
}
|
|
456
|
+
// ────────────────────────────────────────────────────────────────────
|
|
457
|
+
// Reporting
|
|
458
|
+
// ────────────────────────────────────────────────────────────────────
|
|
459
|
+
/**
|
|
460
|
+
* Generate a complete performance report for the session.
|
|
461
|
+
* @param sessionId - Session identifier
|
|
462
|
+
* @returns Structured performance report
|
|
463
|
+
*/
|
|
464
|
+
generateReport(sessionId) {
|
|
465
|
+
const totalDurationMs = this.getTotalDuration();
|
|
466
|
+
const bottlenecks = this.getBottlenecks();
|
|
467
|
+
const parallelHints = this.getParallelHints(new Map());
|
|
468
|
+
// Token summary
|
|
469
|
+
const byPhase = {};
|
|
470
|
+
let totalTokens = 0;
|
|
471
|
+
for (const phase of this.completedPhases) {
|
|
472
|
+
const phaseTotal = phase.tokensUsed.input + phase.tokensUsed.output;
|
|
473
|
+
byPhase[phase.phase] = phaseTotal;
|
|
474
|
+
totalTokens += phaseTotal;
|
|
475
|
+
}
|
|
476
|
+
const wasteEstimate = this.estimateTokenWaste();
|
|
477
|
+
// Cache stats
|
|
478
|
+
const totalCacheOps = this.globalCacheHits + this.globalCacheMisses;
|
|
479
|
+
const hitRate = totalCacheOps > 0 ? this.globalCacheHits / totalCacheOps : 0;
|
|
480
|
+
const savedMs = this.globalCacheHits * AVG_CACHE_HIT_SAVING_MS;
|
|
481
|
+
const suggestions = this.generateSuggestions(bottlenecks);
|
|
482
|
+
return {
|
|
483
|
+
sessionId,
|
|
484
|
+
totalDurationMs,
|
|
485
|
+
phases: [...this.completedPhases],
|
|
486
|
+
bottlenecks,
|
|
487
|
+
cacheStats: {
|
|
488
|
+
hits: this.globalCacheHits,
|
|
489
|
+
misses: this.globalCacheMisses,
|
|
490
|
+
hitRate: Math.round(hitRate * 1000) / 1000,
|
|
491
|
+
savedMs,
|
|
492
|
+
},
|
|
493
|
+
parallelHints,
|
|
494
|
+
tokenSummary: {
|
|
495
|
+
total: totalTokens,
|
|
496
|
+
byPhase,
|
|
497
|
+
wasteEstimate,
|
|
498
|
+
},
|
|
499
|
+
efficiencyScore: this.getEfficiencyScore(),
|
|
500
|
+
suggestions,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Format a performance report as a human-readable string.
|
|
505
|
+
* @param report - Report to format
|
|
506
|
+
* @returns Multi-line formatted string
|
|
507
|
+
*/
|
|
508
|
+
formatReport(report) {
|
|
509
|
+
const lines = [];
|
|
510
|
+
lines.push("═══════════════════════════════════════════════════");
|
|
511
|
+
lines.push(` YUAN Performance Report — Session: ${report.sessionId}`);
|
|
512
|
+
lines.push("═══════════════════════════════════════════════════");
|
|
513
|
+
lines.push("");
|
|
514
|
+
// Overview
|
|
515
|
+
lines.push(`Total Duration: ${this.formatMs(report.totalDurationMs)}`);
|
|
516
|
+
lines.push(`Efficiency Score: ${report.efficiencyScore}/100`);
|
|
517
|
+
lines.push(`Total Tokens: ${report.tokenSummary.total.toLocaleString()}`);
|
|
518
|
+
lines.push("");
|
|
519
|
+
// Phase breakdown
|
|
520
|
+
lines.push("── Phase Breakdown ──────────────────────────────");
|
|
521
|
+
for (const phase of report.phases) {
|
|
522
|
+
const pct = report.totalDurationMs > 0
|
|
523
|
+
? ((phase.durationMs / report.totalDurationMs) * 100).toFixed(1)
|
|
524
|
+
: "0.0";
|
|
525
|
+
const tokens = phase.tokensUsed.input + phase.tokensUsed.output;
|
|
526
|
+
lines.push(` ${phase.phase.padEnd(15)} ${this.formatMs(phase.durationMs).padStart(10)} (${pct}%) tokens: ${tokens.toLocaleString().padStart(8)} tools: ${phase.toolCalls} cache: ${phase.cacheHits}/${phase.cacheHits + phase.cacheMisses}`);
|
|
527
|
+
}
|
|
528
|
+
lines.push("");
|
|
529
|
+
// Cache stats
|
|
530
|
+
lines.push("── Cache Statistics ─────────────────────────────");
|
|
531
|
+
lines.push(` Hits: ${report.cacheStats.hits} Misses: ${report.cacheStats.misses} Rate: ${(report.cacheStats.hitRate * 100).toFixed(1)}% Saved: ~${this.formatMs(report.cacheStats.savedMs)}`);
|
|
532
|
+
lines.push("");
|
|
533
|
+
// Bottlenecks
|
|
534
|
+
if (report.bottlenecks.length > 0) {
|
|
535
|
+
lines.push("── Bottlenecks ──────────────────────────────────");
|
|
536
|
+
for (const b of report.bottlenecks) {
|
|
537
|
+
const icon = b.impact === "high"
|
|
538
|
+
? "[HIGH]"
|
|
539
|
+
: b.impact === "medium"
|
|
540
|
+
? "[MED] "
|
|
541
|
+
: "[LOW] ";
|
|
542
|
+
lines.push(` ${icon} ${b.issue}`);
|
|
543
|
+
lines.push(` -> ${b.suggestion}`);
|
|
544
|
+
if (b.estimatedSavingMs) {
|
|
545
|
+
lines.push(` -> Potential saving: ~${this.formatMs(b.estimatedSavingMs)}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
lines.push("");
|
|
549
|
+
}
|
|
550
|
+
// Parallel hints
|
|
551
|
+
if (report.parallelHints.length > 0) {
|
|
552
|
+
lines.push("── Parallelization Opportunities ────────────────");
|
|
553
|
+
for (const h of report.parallelHints) {
|
|
554
|
+
lines.push(` Tasks [${h.taskIds.join(", ")}]: ${this.formatMs(h.currentSequentialMs)} -> ${this.formatMs(h.estimatedParallelMs)} (${h.speedupFactor}x speedup)`);
|
|
555
|
+
}
|
|
556
|
+
lines.push("");
|
|
557
|
+
}
|
|
558
|
+
// Token waste
|
|
559
|
+
if (report.tokenSummary.wasteEstimate > 0) {
|
|
560
|
+
lines.push("── Token Waste ──────────────────────────────────");
|
|
561
|
+
lines.push(` Estimated waste: ~${report.tokenSummary.wasteEstimate.toLocaleString()} tokens`);
|
|
562
|
+
lines.push("");
|
|
563
|
+
}
|
|
564
|
+
// Suggestions
|
|
565
|
+
if (report.suggestions.length > 0) {
|
|
566
|
+
lines.push("── Suggestions ──────────────────────────────────");
|
|
567
|
+
for (const s of report.suggestions) {
|
|
568
|
+
lines.push(` * ${s}`);
|
|
569
|
+
}
|
|
570
|
+
lines.push("");
|
|
571
|
+
}
|
|
572
|
+
lines.push("═══════════════════════════════════════════════════");
|
|
573
|
+
return lines.join("\n");
|
|
574
|
+
}
|
|
575
|
+
// ────────────────────────────────────────────────────────────────────
|
|
576
|
+
// Reset & Utility
|
|
577
|
+
// ────────────────────────────────────────────────────────────────────
|
|
578
|
+
/**
|
|
579
|
+
* Store current run duration for future historical comparison, then reset all state.
|
|
580
|
+
*/
|
|
581
|
+
reset() {
|
|
582
|
+
const totalMs = this.getTotalDuration();
|
|
583
|
+
if (totalMs > 0) {
|
|
584
|
+
this.previousRunDurations.push(totalMs);
|
|
585
|
+
// Keep bounded
|
|
586
|
+
if (this.previousRunDurations.length > 100) {
|
|
587
|
+
this.previousRunDurations = this.previousRunDurations.slice(-50);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
this.activePhases.clear();
|
|
591
|
+
this.completedPhases = [];
|
|
592
|
+
this.toolCallRecords = [];
|
|
593
|
+
this.metrics = [];
|
|
594
|
+
this.clearCache();
|
|
595
|
+
this.globalCacheHits = 0;
|
|
596
|
+
this.globalCacheMisses = 0;
|
|
597
|
+
}
|
|
598
|
+
// ────────────────────────────────────────────────────────────────────
|
|
599
|
+
// Private helpers
|
|
600
|
+
// ────────────────────────────────────────────────────────────────────
|
|
601
|
+
/** Add a metric, bounded by MAX_METRICS */
|
|
602
|
+
addMetric(metric) {
|
|
603
|
+
if (this.metrics.length < MAX_METRICS) {
|
|
604
|
+
this.metrics.push(metric);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
/** Compute total duration from completed phases */
|
|
608
|
+
getTotalDuration() {
|
|
609
|
+
if (this.completedPhases.length === 0)
|
|
610
|
+
return 0;
|
|
611
|
+
const start = Math.min(...this.completedPhases.map((p) => p.startTime));
|
|
612
|
+
const end = Math.max(...this.completedPhases.map((p) => p.endTime));
|
|
613
|
+
return end - start;
|
|
614
|
+
}
|
|
615
|
+
/** Generate a content-addressable hash for tool+input */
|
|
616
|
+
hashInput(tool, input) {
|
|
617
|
+
const content = `${tool}:${JSON.stringify(input)}`;
|
|
618
|
+
return createHash("sha256").update(content).digest("hex").slice(0, 32);
|
|
619
|
+
}
|
|
620
|
+
/** Evict LRU cache entries until there's room */
|
|
621
|
+
evictIfNeeded(newEntrySize) {
|
|
622
|
+
// Evict by count
|
|
623
|
+
while (this.cache.size >= this.config.maxCacheSize) {
|
|
624
|
+
this.evictLRU();
|
|
625
|
+
}
|
|
626
|
+
// Evict by memory
|
|
627
|
+
while (this.totalCacheMemory + newEntrySize > this.config.maxCacheMemory &&
|
|
628
|
+
this.cache.size > 0) {
|
|
629
|
+
this.evictLRU();
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/** Remove the least recently used cache entry */
|
|
633
|
+
evictLRU() {
|
|
634
|
+
let oldestKey = null;
|
|
635
|
+
let oldestTime = Infinity;
|
|
636
|
+
for (const [key, entry] of this.cache) {
|
|
637
|
+
if (entry.lastAccessedAt < oldestTime) {
|
|
638
|
+
oldestTime = entry.lastAccessedAt;
|
|
639
|
+
oldestKey = key;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
if (oldestKey) {
|
|
643
|
+
const entry = this.cache.get(oldestKey);
|
|
644
|
+
if (entry) {
|
|
645
|
+
this.totalCacheMemory -= entry.sizeBytes;
|
|
646
|
+
}
|
|
647
|
+
this.cache.delete(oldestKey);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** Count tool calls with duplicate inputs */
|
|
651
|
+
countDuplicateToolCalls() {
|
|
652
|
+
const seen = new Set();
|
|
653
|
+
let dupes = 0;
|
|
654
|
+
for (const rec of this.toolCallRecords) {
|
|
655
|
+
if (seen.has(rec.inputHash)) {
|
|
656
|
+
dupes++;
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
seen.add(rec.inputHash);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return dupes;
|
|
663
|
+
}
|
|
664
|
+
/** Estimate wasted tokens from redundant tool calls */
|
|
665
|
+
estimateTokenWaste() {
|
|
666
|
+
// Each redundant tool call wastes ~input description + output tokens
|
|
667
|
+
const dupes = this.countDuplicateToolCalls();
|
|
668
|
+
// Rough estimate: 500 tokens per redundant call (tool description + result)
|
|
669
|
+
return dupes * 500;
|
|
670
|
+
}
|
|
671
|
+
/** Generate actionable suggestions from bottleneck analysis */
|
|
672
|
+
generateSuggestions(bottlenecks) {
|
|
673
|
+
const suggestions = [];
|
|
674
|
+
// From bottlenecks
|
|
675
|
+
for (const b of bottlenecks) {
|
|
676
|
+
if (b.impact === "high") {
|
|
677
|
+
suggestions.push(b.suggestion);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
// Cache suggestions
|
|
681
|
+
const totalCacheOps = this.globalCacheHits + this.globalCacheMisses;
|
|
682
|
+
if (totalCacheOps > 10) {
|
|
683
|
+
const hitRate = this.globalCacheHits / totalCacheOps;
|
|
684
|
+
if (hitRate < 0.3) {
|
|
685
|
+
suggestions.push("Cache hit rate is below 30%. Consider enabling caching or increasing cache size.");
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
else if (totalCacheOps === 0 && this.toolCallRecords.length > 10) {
|
|
689
|
+
suggestions.push("No cache usage detected. Enable caching to avoid redundant tool calls.");
|
|
690
|
+
}
|
|
691
|
+
// Token suggestions
|
|
692
|
+
let totalTokens = 0;
|
|
693
|
+
for (const phase of this.completedPhases) {
|
|
694
|
+
totalTokens +=
|
|
695
|
+
phase.tokensUsed.input + phase.tokensUsed.output;
|
|
696
|
+
}
|
|
697
|
+
if (totalTokens > 200_000) {
|
|
698
|
+
suggestions.push("Token usage exceeds 200k. Consider context compression or summarizing intermediate results.");
|
|
699
|
+
}
|
|
700
|
+
// Phase balance
|
|
701
|
+
if (this.completedPhases.length > 1) {
|
|
702
|
+
const totalMs = this.getTotalDuration();
|
|
703
|
+
const implementPhase = this.completedPhases.find((p) => p.phase === "implement");
|
|
704
|
+
if (implementPhase &&
|
|
705
|
+
totalMs > 0 &&
|
|
706
|
+
implementPhase.durationMs / totalMs < 0.3) {
|
|
707
|
+
suggestions.push("Implementation phase is less than 30% of total time. Analysis/planning may be over-scoped.");
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return suggestions.slice(0, MAX_SUGGESTIONS);
|
|
711
|
+
}
|
|
712
|
+
/** Format milliseconds as human-readable string */
|
|
713
|
+
formatMs(ms) {
|
|
714
|
+
if (ms < 1000)
|
|
715
|
+
return `${ms}ms`;
|
|
716
|
+
if (ms < 60_000)
|
|
717
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
718
|
+
return `${(ms / 60_000).toFixed(1)}m`;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
//# sourceMappingURL=perf-optimizer.js.map
|