@miphamai/cli 0.27.0 → 0.28.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/package.json +1 -1
- package/skills/mipham/self-audit.mipham-skill.md +171 -0
- package/src/core/auto-memory.ts +613 -0
- package/src/core/crsi-sandbox.ts +546 -0
- package/src/core/engine.ts +20 -0
- package/src/ui/input.tsx +61 -8
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRSI Phase 2: Auto-Reflection Engine
|
|
3
|
+
*
|
|
4
|
+
* Structured API for post-turn conversation reflection, CRSI pipeline
|
|
5
|
+
* integration, and persistent memory management. Builds on top of the
|
|
6
|
+
* Phase 1 rule-level self-improvement (PatternAnalyzer, RuleEngine,
|
|
7
|
+
* EffectivenessTracker) to enable code-level and session-level learning.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* Turn completes → analyzeTurn() → persist() → CRSI pipeline feed
|
|
11
|
+
* AI does the thinking; this module provides storage + integration.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { MemoryManager, type MemoryEntry } from './memory/memory-manager.js'
|
|
15
|
+
import type { PatternAnalyzer, Pattern } from '../agent/pattern-analyzer.js'
|
|
16
|
+
import type { ExperienceRuleEngine, ToolRule } from './rule-engine.js'
|
|
17
|
+
import type { EffectivenessTracker } from '../agent/effectiveness-tracker.js'
|
|
18
|
+
import { join } from 'node:path'
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
|
|
21
|
+
// ── Types ──
|
|
22
|
+
|
|
23
|
+
export interface ToolCallRecord {
|
|
24
|
+
/** Tool name (Bash, Read, Write, Edit, Agent, etc.) */
|
|
25
|
+
name: string
|
|
26
|
+
/** Tool input parameters */
|
|
27
|
+
input: Record<string, unknown>
|
|
28
|
+
/** Whether the tool executed successfully */
|
|
29
|
+
success: boolean
|
|
30
|
+
/** Error message if the tool failed */
|
|
31
|
+
error?: string
|
|
32
|
+
/** Duration in ms */
|
|
33
|
+
durationMs?: number
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface CrsiInsight {
|
|
37
|
+
/** Category: timeout | tool-params | search | import | semantic */
|
|
38
|
+
category: string
|
|
39
|
+
/** Human-readable description of the insight */
|
|
40
|
+
description: string
|
|
41
|
+
/** Severity: 'info' | 'warning' | 'critical' */
|
|
42
|
+
severity: 'info' | 'warning' | 'critical'
|
|
43
|
+
/** Suggested action (e.g. "增加 Bash timeout 到 300000ms") */
|
|
44
|
+
suggestion: string
|
|
45
|
+
/** Evidence: concrete examples supporting this insight */
|
|
46
|
+
evidence: string[]
|
|
47
|
+
/** Whether this insight can be auto-applied as a CRSI rule */
|
|
48
|
+
autoApplicable: boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TurnReflection {
|
|
52
|
+
/** Unique ID for this reflection */
|
|
53
|
+
id: string
|
|
54
|
+
/** Session ID */
|
|
55
|
+
sessionId: string
|
|
56
|
+
/** Timestamp */
|
|
57
|
+
timestamp: string
|
|
58
|
+
/** Summary of the turn (1-3 sentences) */
|
|
59
|
+
summary: string
|
|
60
|
+
/** What went well */
|
|
61
|
+
successes: string[]
|
|
62
|
+
/** What failed or could be improved */
|
|
63
|
+
failures: string[]
|
|
64
|
+
/** CRSI-specific insights for rule generation */
|
|
65
|
+
crsiInsights: CrsiInsight[]
|
|
66
|
+
/** Key decisions made during this turn */
|
|
67
|
+
decisions: string[]
|
|
68
|
+
/** Action items for future sessions */
|
|
69
|
+
actionItems: string[]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Constants ──
|
|
73
|
+
|
|
74
|
+
const DEFAULT_MEMORY_DIR = join(homedir(), '.mipham', 'memory')
|
|
75
|
+
|
|
76
|
+
/** Minimum number of similar failures before a CRSI rule is generated. */
|
|
77
|
+
const CRSI_RULE_THRESHOLD = 2
|
|
78
|
+
|
|
79
|
+
/** How many past reflections to recall for context. */
|
|
80
|
+
const MAX_RECALL = 5
|
|
81
|
+
|
|
82
|
+
// ── Engine ──
|
|
83
|
+
|
|
84
|
+
export class AutoMemoryEngine {
|
|
85
|
+
private memoryManager: MemoryManager
|
|
86
|
+
private patternAnalyzer?: PatternAnalyzer
|
|
87
|
+
private ruleEngine?: ExperienceRuleEngine
|
|
88
|
+
private effectivenessTracker?: EffectivenessTracker
|
|
89
|
+
|
|
90
|
+
/** Accumulated reflections for the current session (not yet persisted to disk). */
|
|
91
|
+
private sessionReflections: TurnReflection[] = []
|
|
92
|
+
|
|
93
|
+
constructor(memoryDir: string = DEFAULT_MEMORY_DIR) {
|
|
94
|
+
this.memoryManager = new MemoryManager(memoryDir)
|
|
95
|
+
this.memoryManager.loadAll()
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── CRSI Pipeline Wiring ──
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Wire the CRSI Phase 1 pipeline into the auto-memory engine.
|
|
102
|
+
* Enables insights → rule generation and effectiveness tracking.
|
|
103
|
+
*/
|
|
104
|
+
setCrsiPipeline(
|
|
105
|
+
patternAnalyzer: PatternAnalyzer,
|
|
106
|
+
ruleEngine: ExperienceRuleEngine,
|
|
107
|
+
effectivenessTracker: EffectivenessTracker,
|
|
108
|
+
): void {
|
|
109
|
+
this.patternAnalyzer = patternAnalyzer
|
|
110
|
+
this.ruleEngine = ruleEngine
|
|
111
|
+
this.effectivenessTracker = effectivenessTracker
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Core API ──
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Analyze a completed conversation turn and produce a structured reflection.
|
|
118
|
+
*
|
|
119
|
+
* The AI calls this after each response completes. The engine:
|
|
120
|
+
* 1. Analyzes tool call patterns (successes, failures, timeouts)
|
|
121
|
+
* 2. Cross-references with CRSI pipeline for rule suggestions
|
|
122
|
+
* 3. Produces a TurnReflection ready for persistence
|
|
123
|
+
*/
|
|
124
|
+
analyzeTurn(params: {
|
|
125
|
+
sessionId: string
|
|
126
|
+
userMessage: string
|
|
127
|
+
assistantContent: string
|
|
128
|
+
toolCalls: ToolCallRecord[]
|
|
129
|
+
modelProvider: string
|
|
130
|
+
modelId: string
|
|
131
|
+
turnDurationMs: number
|
|
132
|
+
}): TurnReflection {
|
|
133
|
+
const { sessionId, userMessage, assistantContent, toolCalls, modelProvider, modelId } = params
|
|
134
|
+
|
|
135
|
+
// 1. Categorize tool calls
|
|
136
|
+
const successes: string[] = []
|
|
137
|
+
const failures: string[] = []
|
|
138
|
+
for (const tc of toolCalls) {
|
|
139
|
+
if (tc.success) {
|
|
140
|
+
successes.push(`${tc.name}: ${this.summarizeToolInput(tc)}`)
|
|
141
|
+
} else {
|
|
142
|
+
failures.push(`${tc.name}: ${tc.error || 'unknown error'} — ${this.summarizeToolInput(tc)}`)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 2. Extract CRSI insights from tool failures
|
|
147
|
+
const crsiInsights = this.extractCrsiInsights(toolCalls)
|
|
148
|
+
|
|
149
|
+
// 3. Cross-reference with existing CRSI rules
|
|
150
|
+
this.crossReferenceWithRules(crsiInsights)
|
|
151
|
+
|
|
152
|
+
// 4. Analyze for decisions and action items
|
|
153
|
+
const decisions = this.extractDecisions(userMessage, assistantContent)
|
|
154
|
+
const actionItems = this.extractActionItems(assistantContent, toolCalls)
|
|
155
|
+
|
|
156
|
+
// 5. Build summary
|
|
157
|
+
const summary = this.buildSummary(successes, failures, crsiInsights, decisions)
|
|
158
|
+
|
|
159
|
+
const reflection: TurnReflection = {
|
|
160
|
+
id: `reflection-${sessionId}-${Date.now().toString(36)}`,
|
|
161
|
+
sessionId,
|
|
162
|
+
timestamp: new Date().toISOString(),
|
|
163
|
+
summary,
|
|
164
|
+
successes,
|
|
165
|
+
failures,
|
|
166
|
+
crsiInsights,
|
|
167
|
+
decisions,
|
|
168
|
+
actionItems,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
this.sessionReflections.push(reflection)
|
|
172
|
+
return reflection
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Persist a turn reflection to the memory store.
|
|
177
|
+
*
|
|
178
|
+
* Writes a structured .md file with frontmatter to ~/.mipham/memory/
|
|
179
|
+
* and updates the MEMORY.md index. CRSI insights that meet the threshold
|
|
180
|
+
* are fed into the rule engine for auto-rule generation.
|
|
181
|
+
*/
|
|
182
|
+
persist(reflection: TurnReflection): void {
|
|
183
|
+
const content = this.formatReflectionContent(reflection)
|
|
184
|
+
const relevance = this.extractRelevance(reflection)
|
|
185
|
+
|
|
186
|
+
this.memoryManager.write(reflection.id, content, {
|
|
187
|
+
type: 'feedback',
|
|
188
|
+
relevance,
|
|
189
|
+
why: `CRSI Phase 2 自动复盘 — 会话 ${reflection.sessionId}`,
|
|
190
|
+
howToApply: '下次会话开始时回顾此文件,检查是否有可操作的改进项',
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Feed CRSI insights into the rule engine
|
|
194
|
+
this.feedCrsiPipeline(reflection)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Persist ALL accumulated session reflections at once (e.g. on session end).
|
|
199
|
+
*/
|
|
200
|
+
persistAll(): void {
|
|
201
|
+
for (const reflection of this.sessionReflections) {
|
|
202
|
+
this.persist(reflection)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Also write a session-level summary
|
|
206
|
+
if (this.sessionReflections.length > 0) {
|
|
207
|
+
this.writeSessionSummary()
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Flush CRSI state
|
|
211
|
+
if (this.effectivenessTracker) {
|
|
212
|
+
this.effectivenessTracker.persist()
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Recall relevant past reflections for a given context.
|
|
218
|
+
*/
|
|
219
|
+
recall(context: string, limit: number = MAX_RECALL): MemoryEntry[] {
|
|
220
|
+
return this.memoryManager.recall(context, limit)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build a system reminder string from past reflections for injection
|
|
225
|
+
* into the next session's system prompt.
|
|
226
|
+
*/
|
|
227
|
+
buildReminder(context: string, maxTokens?: number): string {
|
|
228
|
+
return this.memoryManager.buildSystemReminder(context, maxTokens)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get the session reflection count (useful for stats).
|
|
233
|
+
*/
|
|
234
|
+
get sessionReflectionCount(): number {
|
|
235
|
+
return this.sessionReflections.length
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get accumulated CRSI insights across all session reflections.
|
|
240
|
+
*/
|
|
241
|
+
get accumulatedInsights(): CrsiInsight[] {
|
|
242
|
+
return this.sessionReflections.flatMap((r) => r.crsiInsights)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ── Private: CRSI Integration ──
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Extract CRSI insights from tool call records.
|
|
249
|
+
*
|
|
250
|
+
* Identifies patterns like:
|
|
251
|
+
* - timeout: heavy commands without explicit timeout
|
|
252
|
+
* - tool-params: git --force without sandbox disable
|
|
253
|
+
* - import: ESM imports missing .js extension (from Edit/Write errors)
|
|
254
|
+
* - search: full-repo Grep without directory scoping
|
|
255
|
+
*/
|
|
256
|
+
private extractCrsiInsights(toolCalls: ToolCallRecord[]): CrsiInsight[] {
|
|
257
|
+
const insights: CrsiInsight[] = []
|
|
258
|
+
const seen = new Set<string>()
|
|
259
|
+
|
|
260
|
+
for (const tc of toolCalls) {
|
|
261
|
+
if (tc.success) continue
|
|
262
|
+
|
|
263
|
+
const category = this.categorizeFailure(tc)
|
|
264
|
+
const key = `${category}:${tc.name}`
|
|
265
|
+
if (seen.has(key)) continue
|
|
266
|
+
seen.add(key)
|
|
267
|
+
|
|
268
|
+
const evidence = this.gatherEvidence(tc, toolCalls)
|
|
269
|
+
const autoApplicable = ['timeout', 'tool-params'].includes(category)
|
|
270
|
+
|
|
271
|
+
const insight: CrsiInsight = {
|
|
272
|
+
category,
|
|
273
|
+
description: `${tc.name} 工具调用失败: ${tc.error || 'unknown'}`,
|
|
274
|
+
severity: tc.error?.includes('timeout') ? 'critical' : 'warning',
|
|
275
|
+
suggestion: this.suggestFix(category, tc),
|
|
276
|
+
evidence,
|
|
277
|
+
autoApplicable,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
insights.push(insight)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return insights
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Cross-reference new insights with existing CRSI rules.
|
|
288
|
+
* If a similar rule already exists, mark the insight as already-covered.
|
|
289
|
+
*/
|
|
290
|
+
private crossReferenceWithRules(insights: CrsiInsight[]): void {
|
|
291
|
+
if (!this.ruleEngine) return
|
|
292
|
+
|
|
293
|
+
const activeRules = this.ruleEngine.getActiveRules()
|
|
294
|
+
for (const insight of insights) {
|
|
295
|
+
const covered = activeRules.some((r) => r.category === insight.category)
|
|
296
|
+
if (covered) {
|
|
297
|
+
insight.description += ' (已有对应 CRSI 规则覆盖)'
|
|
298
|
+
insight.autoApplicable = false // Don't create duplicate rules
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Feed CRSI insights into the rule engine for automatic rule generation.
|
|
305
|
+
* Only insights that are auto-applicable and have sufficient evidence
|
|
306
|
+
* (≥ CRSI_RULE_THRESHOLD similar failures) are converted to rules.
|
|
307
|
+
*/
|
|
308
|
+
private feedCrsiPipeline(reflection: TurnReflection): void {
|
|
309
|
+
if (!this.ruleEngine || !this.patternAnalyzer) return
|
|
310
|
+
|
|
311
|
+
// Count failures by category across ALL reflections (not just this one)
|
|
312
|
+
const categoryCounts = new Map<string, number>()
|
|
313
|
+
for (const r of this.sessionReflections) {
|
|
314
|
+
for (const insight of r.crsiInsights) {
|
|
315
|
+
if (!insight.autoApplicable) continue
|
|
316
|
+
const count = categoryCounts.get(insight.category) || 0
|
|
317
|
+
categoryCounts.set(insight.category, count + 1)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Generate rules for categories that meet the threshold
|
|
322
|
+
for (const [category, count] of categoryCounts) {
|
|
323
|
+
if (count < CRSI_RULE_THRESHOLD) continue
|
|
324
|
+
|
|
325
|
+
// Use PatternAnalyzer to generate a proper ToolRule
|
|
326
|
+
const pattern: Pattern = {
|
|
327
|
+
id: `auto-${category}-${Date.now().toString(36)}`,
|
|
328
|
+
category: category as Pattern['category'],
|
|
329
|
+
agentName: 'auto-memory',
|
|
330
|
+
frequency: count,
|
|
331
|
+
confidence: count >= 5 ? 'high' : 'medium',
|
|
332
|
+
examples: this.sessionReflections
|
|
333
|
+
.flatMap((r) => r.crsiInsights)
|
|
334
|
+
.filter((i) => i.category === category)
|
|
335
|
+
.flatMap((i) => i.evidence)
|
|
336
|
+
.slice(0, 5),
|
|
337
|
+
firstSeen: this.sessionReflections[0]?.timestamp || '',
|
|
338
|
+
lastSeen: this.sessionReflections[this.sessionReflections.length - 1]?.timestamp || '',
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const toolRule = this.patternAnalyzer.toToolRule(pattern)
|
|
342
|
+
this.ruleEngine.register(toolRule)
|
|
343
|
+
|
|
344
|
+
// Track the new rule's effectiveness
|
|
345
|
+
if (this.effectivenessTracker) {
|
|
346
|
+
this.effectivenessTracker.recordApplication(toolRule.id, true)
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ── Private: Analysis Helpers ──
|
|
352
|
+
|
|
353
|
+
private categorizeFailure(tc: ToolCallRecord): string {
|
|
354
|
+
const err = (tc.error || '').toLowerCase()
|
|
355
|
+
const cmd = String(tc.input?.command || tc.input?.description || '').toLowerCase()
|
|
356
|
+
|
|
357
|
+
if (err.includes('timeout') || err.includes('timed out')) return 'timeout'
|
|
358
|
+
if (cmd.includes('--force') || cmd.includes('rm -rf')) return 'tool-params'
|
|
359
|
+
if (err.includes('import') || err.includes('module') || err.includes('.js')) return 'import'
|
|
360
|
+
if (tc.name === 'Grep' && err.includes('no matches')) return 'search'
|
|
361
|
+
if (err.includes('permission') || err.includes('denied')) return 'tool-params'
|
|
362
|
+
|
|
363
|
+
return 'semantic'
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private gatherEvidence(tc: ToolCallRecord, allCalls: ToolCallRecord[]): string[] {
|
|
367
|
+
const evidence: string[] = []
|
|
368
|
+
evidence.push(`${tc.name}: ${this.summarizeToolInput(tc)}`)
|
|
369
|
+
|
|
370
|
+
// Find similar failures in the same batch
|
|
371
|
+
const similar = allCalls.filter(
|
|
372
|
+
(c) => c !== tc && !c.success && this.categorizeFailure(c) === this.categorizeFailure(tc),
|
|
373
|
+
)
|
|
374
|
+
if (similar.length > 0) {
|
|
375
|
+
evidence.push(`同一轮中还有 ${similar.length} 个同类失败`)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return evidence
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private suggestFix(category: string, tc: ToolCallRecord): string {
|
|
382
|
+
switch (category) {
|
|
383
|
+
case 'timeout':
|
|
384
|
+
return `为 ${tc.name} 增加 timeout 参数至 300000ms(5分钟)`
|
|
385
|
+
case 'tool-params':
|
|
386
|
+
return `检查 ${tc.name} 的参数安全性,考虑添加 dangerouslyDisableSandbox`
|
|
387
|
+
case 'import':
|
|
388
|
+
return '检查 ESM 模块导入是否缺少 .js 扩展名'
|
|
389
|
+
case 'search':
|
|
390
|
+
return '建议先用 Glob 缩小搜索范围,再使用 Grep'
|
|
391
|
+
default:
|
|
392
|
+
return '人工审查此工具调用的参数和上下文'
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private extractDecisions(userMessage: string, assistantContent: string): string[] {
|
|
397
|
+
const decisions: string[] = []
|
|
398
|
+
const decisionPatterns = [
|
|
399
|
+
/决定使用\s*(.+)/g,
|
|
400
|
+
/选择\s*(.+?)(?:作为|方案)/g,
|
|
401
|
+
/采用\s*(.+?)(?:方案|架构|设计)/g,
|
|
402
|
+
/decided?\s*(?:to|on)\s*(.+)/gi,
|
|
403
|
+
/opted\s*(?:for|to)\s*(.+)/gi,
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
for (const pattern of decisionPatterns) {
|
|
407
|
+
let match: RegExpExecArray | null
|
|
408
|
+
while ((match = pattern.exec(assistantContent)) !== null) {
|
|
409
|
+
decisions.push(match[1]!.trim())
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Also check for explicit decisions in user message
|
|
414
|
+
const userDecisionMatch = userMessage.match(/用\s*(.+?)(?:吧|方案|方法)/)
|
|
415
|
+
if (userDecisionMatch?.[1]) {
|
|
416
|
+
decisions.push(`用户决定: ${userDecisionMatch[1].trim()}`)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
return decisions.slice(0, 5)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private extractActionItems(
|
|
423
|
+
assistantContent: string,
|
|
424
|
+
toolCalls: ToolCallRecord[],
|
|
425
|
+
): string[] {
|
|
426
|
+
const items: string[] = []
|
|
427
|
+
|
|
428
|
+
// Check for TODO markers in assistant content
|
|
429
|
+
const todoMatches = assistantContent.matchAll(/[-*]\s*\[ \]\s*(.+)/g)
|
|
430
|
+
for (const match of todoMatches) {
|
|
431
|
+
items.push(match[1]!.trim())
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Failed tool calls become action items
|
|
435
|
+
for (const tc of toolCalls) {
|
|
436
|
+
if (!tc.success) {
|
|
437
|
+
items.push(`修复 ${tc.name} 调用失败: ${tc.error || 'unknown'}`)
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return items.slice(0, 8)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private buildSummary(
|
|
445
|
+
successes: string[],
|
|
446
|
+
failures: string[],
|
|
447
|
+
insights: CrsiInsight[],
|
|
448
|
+
decisions: string[],
|
|
449
|
+
): string {
|
|
450
|
+
const parts: string[] = []
|
|
451
|
+
|
|
452
|
+
if (successes.length > 0) {
|
|
453
|
+
parts.push(`${successes.length} 个工具调用成功`)
|
|
454
|
+
}
|
|
455
|
+
if (failures.length > 0) {
|
|
456
|
+
parts.push(`${failures.length} 个失败`)
|
|
457
|
+
}
|
|
458
|
+
if (insights.length > 0) {
|
|
459
|
+
const criticalCount = insights.filter((i) => i.severity === 'critical').length
|
|
460
|
+
if (criticalCount > 0) {
|
|
461
|
+
parts.push(`${criticalCount} 个关键 CRSI 洞察`)
|
|
462
|
+
} else {
|
|
463
|
+
parts.push(`${insights.length} 个 CRSI 洞察`)
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
if (decisions.length > 0) {
|
|
467
|
+
parts.push(`${decisions.length} 个关键决策`)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return parts.length > 0 ? parts.join(',') : '本轮无特殊事件'
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private summarizeToolInput(tc: ToolCallRecord): string {
|
|
474
|
+
switch (tc.name) {
|
|
475
|
+
case 'Bash':
|
|
476
|
+
return String(tc.input?.command || '').slice(0, 80)
|
|
477
|
+
case 'Read':
|
|
478
|
+
return String(tc.input?.file_path || '').slice(0, 80)
|
|
479
|
+
case 'Write':
|
|
480
|
+
return String(tc.input?.file_path || '').slice(0, 80)
|
|
481
|
+
case 'Edit':
|
|
482
|
+
return `${String(tc.input?.file_path || '').slice(0, 60)}: ${String(tc.input?.old_string || '').slice(0, 20)}...`
|
|
483
|
+
case 'Grep':
|
|
484
|
+
return String(tc.input?.pattern || '').slice(0, 80)
|
|
485
|
+
case 'Agent':
|
|
486
|
+
return String(tc.input?.description || tc.input?.prompt || '').slice(0, 80)
|
|
487
|
+
default:
|
|
488
|
+
return JSON.stringify(tc.input).slice(0, 80)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ── Private: Persistence ──
|
|
493
|
+
|
|
494
|
+
private formatReflectionContent(reflection: TurnReflection): string {
|
|
495
|
+
const lines: string[] = [
|
|
496
|
+
`# 会话复盘: ${reflection.id}`,
|
|
497
|
+
'',
|
|
498
|
+
`**会话**: ${reflection.sessionId}`,
|
|
499
|
+
`**时间**: ${reflection.timestamp}`,
|
|
500
|
+
`**摘要**: ${reflection.summary}`,
|
|
501
|
+
'',
|
|
502
|
+
'---',
|
|
503
|
+
'',
|
|
504
|
+
'## 成功项',
|
|
505
|
+
'',
|
|
506
|
+
...(reflection.successes.length > 0
|
|
507
|
+
? reflection.successes.map((s) => `- ✅ ${s}`)
|
|
508
|
+
: ['- _(本轮无记录的成功项)_']),
|
|
509
|
+
'',
|
|
510
|
+
'## 失败项',
|
|
511
|
+
'',
|
|
512
|
+
...(reflection.failures.length > 0
|
|
513
|
+
? reflection.failures.map((f) => `- ❌ ${f}`)
|
|
514
|
+
: ['- _(本轮无失败)_']),
|
|
515
|
+
'',
|
|
516
|
+
'## CRSI 洞察',
|
|
517
|
+
'',
|
|
518
|
+
...(reflection.crsiInsights.length > 0
|
|
519
|
+
? reflection.crsiInsights.map(
|
|
520
|
+
(i) =>
|
|
521
|
+
`- 🔧 [${i.severity === 'critical' ? '⚠️' : '📝'} ${i.category}] ${i.description}\n → 建议: ${i.suggestion}${i.autoApplicable ? ' _(可自动应用)_' : ''}`,
|
|
522
|
+
)
|
|
523
|
+
: ['- _(本轮无 CRSI 洞察)_']),
|
|
524
|
+
'',
|
|
525
|
+
'## 关键决策',
|
|
526
|
+
'',
|
|
527
|
+
...(reflection.decisions.length > 0
|
|
528
|
+
? reflection.decisions.map((d) => `- 🎯 ${d}`)
|
|
529
|
+
: ['- _(本轮无关键决策)_']),
|
|
530
|
+
'',
|
|
531
|
+
'## 待办项',
|
|
532
|
+
'',
|
|
533
|
+
...(reflection.actionItems.length > 0
|
|
534
|
+
? reflection.actionItems.map((a) => `- [ ] ${a}`)
|
|
535
|
+
: ['- _(本轮无待办项)_']),
|
|
536
|
+
'',
|
|
537
|
+
'---',
|
|
538
|
+
'',
|
|
539
|
+
`_由 CRSI Phase 2 AutoMemoryEngine 自动生成_`,
|
|
540
|
+
]
|
|
541
|
+
|
|
542
|
+
return lines.join('\n')
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
private extractRelevance(reflection: TurnReflection): string[] {
|
|
546
|
+
const keywords = new Set<string>()
|
|
547
|
+
|
|
548
|
+
// Extract from CRSI insights
|
|
549
|
+
for (const insight of reflection.crsiInsights) {
|
|
550
|
+
keywords.add(insight.category)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Extract from decisions
|
|
554
|
+
for (const d of reflection.decisions) {
|
|
555
|
+
const words = d.split(/\s+/).filter((w) => w.length > 2)
|
|
556
|
+
for (const w of words.slice(0, 3)) {
|
|
557
|
+
keywords.add(w.toLowerCase())
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Always include core tags
|
|
562
|
+
keywords.add('crsi-phase-2')
|
|
563
|
+
keywords.add('turn-reflection')
|
|
564
|
+
keywords.add(reflection.sessionId)
|
|
565
|
+
|
|
566
|
+
return Array.from(keywords).slice(0, 10)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private writeSessionSummary(): void {
|
|
570
|
+
const totalSuccesses = this.sessionReflections.reduce((s, r) => s + r.successes.length, 0)
|
|
571
|
+
const totalFailures = this.sessionReflections.reduce((s, r) => s + r.failures.length, 0)
|
|
572
|
+
const totalInsights = this.sessionReflections.reduce((s, r) => s + r.crsiInsights.length, 0)
|
|
573
|
+
const criticalInsights = this.sessionReflections.flatMap((r) =>
|
|
574
|
+
r.crsiInsights.filter((i) => i.severity === 'critical'),
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
const content = [
|
|
578
|
+
`# 会话总结`,
|
|
579
|
+
'',
|
|
580
|
+
`**会话 ID**: ${this.sessionReflections[0]?.sessionId || 'unknown'}`,
|
|
581
|
+
`**复盘数**: ${this.sessionReflections.length}`,
|
|
582
|
+
`**时间**: ${new Date().toISOString()}`,
|
|
583
|
+
'',
|
|
584
|
+
'---',
|
|
585
|
+
'',
|
|
586
|
+
'## 统计',
|
|
587
|
+
'',
|
|
588
|
+
`| 指标 | 数值 |`,
|
|
589
|
+
`|------|------|`,
|
|
590
|
+
`| 成功工具调用 | ${totalSuccesses} |`,
|
|
591
|
+
`| 失败工具调用 | ${totalFailures} |`,
|
|
592
|
+
`| CRSI 洞察 | ${totalInsights} |`,
|
|
593
|
+
`| 关键洞察 | ${criticalInsights.length} |`,
|
|
594
|
+
`| 成功率 | ${totalSuccesses + totalFailures > 0 ? Math.round((totalSuccesses / (totalSuccesses + totalFailures)) * 100) : 100}% |`,
|
|
595
|
+
'',
|
|
596
|
+
...(criticalInsights.length > 0
|
|
597
|
+
? ['## ⚠️ 关键洞察', '', ...criticalInsights.map((i) => `- **${i.category}**: ${i.description}\n → ${i.suggestion}`)]
|
|
598
|
+
: []),
|
|
599
|
+
'',
|
|
600
|
+
'---',
|
|
601
|
+
'',
|
|
602
|
+
`_由 CRSI Phase 2 AutoMemoryEngine 在会话结束时自动生成_`,
|
|
603
|
+
].join('\n')
|
|
604
|
+
|
|
605
|
+
const name = `session-summary-${this.sessionReflections[0]?.sessionId || Date.now().toString(36)}`
|
|
606
|
+
this.memoryManager.write(name, content, {
|
|
607
|
+
type: 'feedback',
|
|
608
|
+
relevance: ['session-summary', 'crsi-phase-2', ...criticalInsights.map((i) => i.category)],
|
|
609
|
+
why: 'CRSI Phase 2 会话级自动总结',
|
|
610
|
+
howToApply: '在下次会话开始时回顾,重点关注关键洞察和失败模式',
|
|
611
|
+
})
|
|
612
|
+
}
|
|
613
|
+
}
|