@miphamai/cli 0.33.2 → 0.34.1

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.
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Emotion Detector — User Frustration Detection
3
+ *
4
+ * Inspired by Claude Code's emotion detection: parses user input for signals
5
+ * of frustration, confusion, or satisfaction. Adjusts agent behavior accordingly:
6
+ * - Frustration → be more concise, skip explanations, get to the fix faster
7
+ * - Confusion → explain more, offer context, slow down
8
+ * - Satisfaction → maintain current mode
9
+ *
10
+ * Uses regex heuristics (no LLM call) for zero-latency detection.
11
+ * Designed as a lightweight pre-processor in the input pipeline.
12
+ */
13
+
14
+ // ── Types ──
15
+
16
+ export type Emotion =
17
+ | 'frustrated' // User is angry/annoyed — move fast, skip explanations
18
+ | 'confused' // User doesn't understand — explain more
19
+ | 'impatient' // User wants speed — be terse
20
+ | 'satisfied' // User is happy — continue normally
21
+ | 'neutral' // No strong signal
22
+
23
+ export interface EmotionResult {
24
+ emotion: Emotion
25
+ /** 0.0-1.0 confidence in the detected emotion */
26
+ confidence: number
27
+ /** Specific phrases that triggered the detection */
28
+ triggers: string[]
29
+ /** Suggested behavior adjustment for the agent */
30
+ suggestion: string
31
+ }
32
+
33
+ // ── Constants ──
34
+
35
+ /** Patterns that indicate frustration/anger. Ordered by severity. */
36
+ const FRUSTRATION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
37
+ { pattern: /\b(wtf|wtaf)\b/i, weight: 0.9, label: 'profanity' },
38
+ {
39
+ pattern: /\b(useless|worthless|garbage|rubbish|broken)\b/i,
40
+ weight: 0.8,
41
+ label: 'strong-negative',
42
+ },
43
+ { pattern: /\b(damn|shit|crap)\b/i, weight: 0.7, label: 'mild-profanity' },
44
+ {
45
+ pattern: /\b(stop|don't|do not)\s+(explain|talk|lecture|ramble)\b/i,
46
+ weight: 0.85,
47
+ label: 'stop-explaining',
48
+ },
49
+ {
50
+ pattern: /\b(just\s+fix\s+it|just\s+do\s+it|get\s+on\s+with\s+it)\b/i,
51
+ weight: 0.8,
52
+ label: 'just-fix-it',
53
+ },
54
+ {
55
+ pattern: /\b(no|wrong|incorrect|bad|terrible|awful)\s*[!!]{1,3}/i,
56
+ weight: 0.75,
57
+ label: 'emphatic-no',
58
+ },
59
+ { pattern: /\b(again\?|still|not\s+again)\b/i, weight: 0.7, label: 'repeated-failure' },
60
+ { pattern: /\b(i\s+said|i\s+told\s+you|i\s+already)\b/i, weight: 0.75, label: 'i-already-said' },
61
+ { pattern: /\b(no\s+no\s+no|nope\s+nope)\b/i, weight: 0.7, label: 'repeated-no' },
62
+ { pattern: /[!!]{2,}/, weight: 0.6, label: 'multiple-exclamation' },
63
+ {
64
+ pattern: /\b(why|why\s+would\s+you|what\s+are\s+you\s+doing)\b/i,
65
+ weight: 0.65,
66
+ label: 'why-question',
67
+ },
68
+ { pattern: /[??]{2,}/, weight: 0.4, label: 'multiple-question-marks' },
69
+ ]
70
+
71
+ /** Patterns that indicate confusion / need for more explanation */
72
+ const CONFUSION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
73
+ {
74
+ pattern: /\b(i\s+don'?t\s+understand|i\s+am\s+confused|i'?m\s+confused)\b/i,
75
+ weight: 0.9,
76
+ label: 'explicit-confusion',
77
+ },
78
+ {
79
+ pattern: /\b(what\s+does|what\s+is|what\s+are|how\s+does)\b/i,
80
+ weight: 0.6,
81
+ label: 'what-is-question',
82
+ },
83
+ {
84
+ pattern: /\b(can\s+you\s+explain|explain\s+this|elaborate)\b/i,
85
+ weight: 0.7,
86
+ label: 'ask-for-explanation',
87
+ },
88
+ { pattern: /\b(wait|huh|eh\?+|hmm+)\b/i, weight: 0.5, label: 'hesitation' },
89
+ {
90
+ pattern: /\b(i\s+thought|i\s+expected|shouldn'?t\s+it)\b/i,
91
+ weight: 0.6,
92
+ label: 'mismatch-expectation',
93
+ },
94
+ ]
95
+
96
+ /** Patterns that indicate impatience */
97
+ const IMPATIENCE_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
98
+ { pattern: /\b(hurry|quick|fast|faster|asap|urgent)\b/i, weight: 0.8, label: 'speed-words' },
99
+ {
100
+ pattern: /\b(just\s+the\s+(code|answer|result|fix|summary))\b/i,
101
+ weight: 0.75,
102
+ label: 'just-the-result',
103
+ },
104
+ {
105
+ pattern: /\b(skip\s+(the\s+)?(explanation|details|context|background))\b/i,
106
+ weight: 0.8,
107
+ label: 'skip-explanation',
108
+ },
109
+ { pattern: /\b(tldr|tl;dr|summary|summarize)\b/i, weight: 0.7, label: 'tldr' },
110
+ {
111
+ pattern: /\b(get\s+to\s+the\s+point|cut\s+to\s+the\s+chase)\b/i,
112
+ weight: 0.75,
113
+ label: 'get-to-point',
114
+ },
115
+ ]
116
+
117
+ /** Patterns that indicate satisfaction */
118
+ const SATISFACTION_PATTERNS: Array<{ pattern: RegExp; weight: number; label: string }> = [
119
+ { pattern: /\b(thanks|thank\s+you|thx|ty|tyvm)\b/i, weight: 0.5, label: 'thanks' },
120
+ {
121
+ pattern: /\b(great|awesome|perfect|excellent|amazing|brilliant|beautiful)\b/i,
122
+ weight: 0.6,
123
+ label: 'positive-words',
124
+ },
125
+ { pattern: /\b(nice|good\s+job|well\s+done|love\s+it)\b/i, weight: 0.7, label: 'praise' },
126
+ {
127
+ pattern: /\b(exactly|finally|yes!|that'?s\s+it)\b/i,
128
+ weight: 0.6,
129
+ label: 'confirmation-positive',
130
+ },
131
+ { pattern: /[👍🙌🎉✨💯🔥✅]/u, weight: 0.5, label: 'positive-emoji' },
132
+ ]
133
+
134
+ // ── Detector ──
135
+
136
+ export class EmotionDetector {
137
+ /**
138
+ * Analyze user input for emotional signals.
139
+ *
140
+ * @param input — raw user message text
141
+ * @param recentContext — optional recent conversation turns for pattern detection
142
+ * @returns EmotionResult with detected emotion and behavioral suggestion
143
+ */
144
+ detect(input: string, recentContext?: string[]): EmotionResult {
145
+ // Check frustration first (most actionable)
146
+ const frustration = this.scorePatterns(input, FRUSTRATION_PATTERNS, recentContext)
147
+ if (frustration.score >= 0.5) {
148
+ return {
149
+ emotion: 'frustrated',
150
+ confidence: frustration.score,
151
+ triggers: frustration.triggers,
152
+ suggestion:
153
+ frustration.score >= 0.8
154
+ ? 'User is very frustrated. Be extremely concise. Skip ALL explanations. Output only the fix/result. No preamble, no summary, no "here\'s what I did". One sentence max before code.'
155
+ : 'User is frustrated. Be concise. Skip explanations. Get to the fix immediately.',
156
+ }
157
+ }
158
+
159
+ // Check impatience
160
+ const impatience = this.scorePatterns(input, IMPATIENCE_PATTERNS)
161
+ if (impatience.score >= 0.5) {
162
+ return {
163
+ emotion: 'impatient',
164
+ confidence: impatience.score,
165
+ triggers: impatience.triggers,
166
+ suggestion:
167
+ 'User wants speed. Skip explanations. Output the result immediately. No "let me explain why" — just the answer.',
168
+ }
169
+ }
170
+
171
+ // Check confusion
172
+ const confusion = this.scorePatterns(input, CONFUSION_PATTERNS)
173
+ if (confusion.score >= 0.5) {
174
+ return {
175
+ emotion: 'confused',
176
+ confidence: confusion.score,
177
+ triggers: confusion.triggers,
178
+ suggestion:
179
+ 'User is confused. Explain more. Offer context. Break down complex steps. Ask clarifying questions rather than assuming.',
180
+ }
181
+ }
182
+
183
+ // Check satisfaction
184
+ const satisfaction = this.scorePatterns(input, SATISFACTION_PATTERNS)
185
+ if (satisfaction.score >= 0.5) {
186
+ return {
187
+ emotion: 'satisfied',
188
+ confidence: satisfaction.score,
189
+ triggers: satisfaction.triggers,
190
+ suggestion:
191
+ 'User is satisfied. Maintain current tone and pace. Good time for optional suggestions.',
192
+ }
193
+ }
194
+
195
+ return {
196
+ emotion: 'neutral',
197
+ confidence: 0,
198
+ triggers: [],
199
+ suggestion: 'No strong emotional signal. Proceed normally.',
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Quick check — does this input contain ANY frustration signal?
205
+ * Use as a pre-filter before the full detect() call.
206
+ */
207
+ isFrustrated(input: string): boolean {
208
+ return FRUSTRATION_PATTERNS.some((p) => p.pattern.test(input))
209
+ }
210
+
211
+ /**
212
+ * Detect repeated frustration across multiple turns (escalation detection).
213
+ */
214
+ detectEscalation(recentInputs: string[]): boolean {
215
+ if (recentInputs.length < 2) return false
216
+ const recent = recentInputs.slice(-3)
217
+ const frustrationCount = recent.filter((i) => this.isFrustrated(i)).length
218
+ return frustrationCount >= 2
219
+ }
220
+
221
+ // ── Internal ──
222
+
223
+ private scorePatterns(
224
+ input: string,
225
+ patterns: Array<{ pattern: RegExp; weight: number; label: string }>,
226
+ recentContext?: string[],
227
+ ): { score: number; triggers: string[] } {
228
+ const triggers: string[] = []
229
+ let totalWeight = 0
230
+ let matchCount = 0
231
+
232
+ for (const p of patterns) {
233
+ if (p.pattern.test(input)) {
234
+ triggers.push(p.label)
235
+ totalWeight += p.weight
236
+ matchCount++
237
+ }
238
+ }
239
+
240
+ // Check recent context for frustration escalation
241
+ if (recentContext && patterns === FRUSTRATION_PATTERNS) {
242
+ const recentFrustration = recentContext
243
+ .slice(-2)
244
+ .filter((ctx) => patterns.some((p) => p.pattern.test(ctx))).length
245
+ if (recentFrustration >= 1) {
246
+ totalWeight += 0.15 // escalation bonus
247
+ }
248
+ }
249
+
250
+ // Normalize: first match passes at weight ≥ 0.5; additional matches increase confidence
251
+ // with diminishing returns (matchCount-1 so single match = no dampening)
252
+ const score =
253
+ matchCount === 0 ? 0 : Math.min(1.0, totalWeight / (1 + Math.max(0, matchCount - 1) * 0.5))
254
+
255
+ return { score, triggers }
256
+ }
257
+ }
@@ -15,6 +15,7 @@ import type { AgentRegistry } from '../agent/agent-registry'
15
15
  import { analyzeForMemory } from './memory/memory-writer'
16
16
  import { getMemoryManager } from './memory/memory-loader'
17
17
  import { AutoMemoryEngine } from './auto-memory.js'
18
+ import type { ToolCallRecord } from './auto-memory.js'
18
19
  import type { AgentViewManager } from '../agent-view/agent-view-manager'
19
20
  import type { SkillsLoader } from '../skills/loader'
20
21
  import { getBackgroundAgentRegistry } from '../agent/background-registry'
@@ -26,6 +27,9 @@ import { ErrorSignatureDB } from './error-signature-db.js'
26
27
  import { PreFlightChecker } from './preflight-checker.js'
27
28
  import { AutoCorrector } from './auto-corrector.js'
28
29
  import { MetaRuleEngine } from './meta-rule-engine.js'
30
+ import { DreamEngine } from './dream-engine.js'
31
+ import { ConstitutionLoader } from './constitution-loader.js'
32
+ import { SelfCritique } from './self-critique.js'
29
33
  import { UsageTracker } from './usage-tracker'
30
34
  import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
31
35
  import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
@@ -55,6 +59,9 @@ export class QueryEngine {
55
59
  private _preflightChecker?: PreFlightChecker
56
60
  private _autoCorrector?: AutoCorrector
57
61
  private _metaRuleEngine?: MetaRuleEngine
62
+ private _dreamEngine?: DreamEngine
63
+ private _constitutionLoader?: ConstitutionLoader
64
+ private _selfCritique?: SelfCritique
58
65
  /** Files read this session — tracks what the Read tool has loaded */
59
66
  private readFiles = new Set<string>()
60
67
  private goal?: string
@@ -428,6 +435,7 @@ export class QueryEngine {
428
435
  let thinkingContent = ''
429
436
  let turnApiInputTokens = 0
430
437
  let turnApiOutputTokens = 0
438
+ const turnStart = Date.now()
431
439
  const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
432
440
 
433
441
  // Stream model response
@@ -534,7 +542,9 @@ export class QueryEngine {
534
542
  }
535
543
 
536
544
  // Execute any tools that were requested
545
+ const toolCallRecords: ToolCallRecord[] = []
537
546
  for (const toolUse of toolUses) {
547
+ const toolStart = Date.now()
538
548
  const result = await this.executeTool(toolUse.name, toolUse.input)
539
549
  yield {
540
550
  type: 'tool_result',
@@ -542,6 +552,15 @@ export class QueryEngine {
542
552
  content: result.success ? result.content : result.error || result.content,
543
553
  }
544
554
 
555
+ // Collect tool call record for CRSI auto-reflection
556
+ toolCallRecords.push({
557
+ name: toolUse.name,
558
+ input: toolUse.input,
559
+ success: result.success,
560
+ error: result.success ? undefined : result.error || 'Unknown error',
561
+ durationMs: Date.now() - toolStart,
562
+ })
563
+
545
564
  // Add tool use + result to context
546
565
  // DeepSeek V4 thinking mode requires reasoning_content on every assistant message
547
566
  this.context.addMessage({
@@ -561,6 +580,28 @@ export class QueryEngine {
561
580
  })
562
581
  }
563
582
 
583
+ // ── CRSI Reflection: Wire AutoMemoryEngine.analyzeTurn() into main loop ──
584
+ // This is the primary CRSI learning mechanism — every turn's tool calls,
585
+ // successes, and failures are analyzed for patterns and fed into the
586
+ // CRSI pipeline (PatternAnalyzer → ExperienceRuleEngine → ErrorSignatureDB).
587
+ if (toolCallRecords.length > 0) {
588
+ try {
589
+ const autoMemory = this.getAutoMemory()
590
+ const reflection = autoMemory.analyzeTurn({
591
+ sessionId: this.sessionId,
592
+ userMessage: userInput,
593
+ assistantContent,
594
+ toolCalls: toolCallRecords,
595
+ modelProvider: this.registry.getActive().config.id,
596
+ modelId: this.registry.getActiveModel(),
597
+ turnDurationMs: turnStart ? Date.now() - turnStart : 0,
598
+ })
599
+ autoMemory.persist(reflection)
600
+ } catch {
601
+ // Reflection is non-critical — failures must not break the main loop
602
+ }
603
+ }
604
+
564
605
  // Record API token usage for this turn, attributed to executed tools
565
606
  if (turnApiInputTokens > 0 || turnApiOutputTokens > 0) {
566
607
  if (toolUses.length > 0) {
@@ -936,6 +977,62 @@ export class QueryEngine {
936
977
  hookWarnings = [...hookWarnings, preflight.warning]
937
978
  }
938
979
 
980
+ // ── Mipham Constitution — constitutional principle enforcement ──
981
+ // Checks the tool + params against the machine-readable constitution.
982
+ // Block-level violations (enforce: block) halt execution immediately.
983
+ // Warn-level violations (enforce: warn) are appended to hookWarnings.
984
+ const constitution = this.getConstitutionLoader()
985
+ const principles = constitution.getPrinciplesForTool(name)
986
+ for (const principle of principles) {
987
+ if (principle.enforce === 'block') {
988
+ // Audit: check if the tool params or command match a violation pattern
989
+ if (principle.audit_pattern) {
990
+ try {
991
+ const regex = new RegExp(principle.audit_pattern, 'i')
992
+ const paramsStr = JSON.stringify(effectiveParams)
993
+ if (regex.test(paramsStr)) {
994
+ return {
995
+ success: false,
996
+ content: `🚫 Constitution violation blocked: **${principle.id}** — ${principle.text}`,
997
+ error: `Constitutional principle "${principle.id}" blocked this operation.`,
998
+ }
999
+ }
1000
+ } catch {
1001
+ // Invalid regex — skip this principle
1002
+ }
1003
+ }
1004
+ }
1005
+ if (principle.enforce === 'warn') {
1006
+ hookWarnings = [...hookWarnings, `⚖️ Constitution: ${principle.id} — ${principle.text}`]
1007
+ }
1008
+ }
1009
+
1010
+ // ── Self-Critique Hook — RLAIF-style tool-call safety verification ──
1011
+ // Uses a fast model (Flash) to critique the tool call before execution.
1012
+ // Safe + correct + necessary check with <200ms target latency.
1013
+ // Fail-open: if the critique times out or errors, the tool still executes.
1014
+ const selfCritique = this.getSelfCritique()
1015
+ if (selfCritique.getConfig().enabled) {
1016
+ const critiqueResult = await selfCritique.critique(name, effectiveParams, this.registry)
1017
+ if (critiqueResult) {
1018
+ if (critiqueResult.score < selfCritique.getConfig().threshold) {
1019
+ // Score too low — block with explanation
1020
+ if (!critiqueResult.safe) {
1021
+ return {
1022
+ success: false,
1023
+ content: `🔍 Self-Critique blocked: ${critiqueResult.reasoning}`,
1024
+ error: `Self-Critique safety check failed (score: ${(critiqueResult.score * 100).toFixed(0)}%). ${critiqueResult.correction || ''}`,
1025
+ }
1026
+ }
1027
+ // Score low but safe — warn but allow
1028
+ hookWarnings = [
1029
+ ...hookWarnings,
1030
+ `🔍 Self-Critique: ${critiqueResult.reasoning}${critiqueResult.correction ? ` — Suggestion: ${critiqueResult.correction}` : ''}`,
1031
+ ]
1032
+ }
1033
+ }
1034
+ }
1035
+
939
1036
  try {
940
1037
  const result = await tool.execute(effectiveParams, {
941
1038
  cwd: process.cwd(),
@@ -1103,7 +1200,7 @@ export class QueryEngine {
1103
1200
  * Checks tool calls against ErrorSignatureDB + ExperienceRuleEngine
1104
1201
  * before execution, enabling preventive error interception.
1105
1202
  */
1106
- private getPreFlightChecker(): PreFlightChecker {
1203
+ getPreFlightChecker(): PreFlightChecker {
1107
1204
  if (!this._preflightChecker) {
1108
1205
  this._preflightChecker = new PreFlightChecker(
1109
1206
  this.getErrorSignatureDB(),
@@ -1141,6 +1238,30 @@ export class QueryEngine {
1141
1238
  return this._metaRuleEngine
1142
1239
  }
1143
1240
 
1241
+ /** Auto-Dream: Lazily-initialized background memory consolidation engine. */
1242
+ getDreamEngine(): DreamEngine {
1243
+ if (!this._dreamEngine) {
1244
+ this._dreamEngine = new DreamEngine()
1245
+ }
1246
+ return this._dreamEngine
1247
+ }
1248
+
1249
+ /** Mipham Constitution: Lazily-initialized constitutional principle loader/enforcer. */
1250
+ getConstitutionLoader(): ConstitutionLoader {
1251
+ if (!this._constitutionLoader) {
1252
+ this._constitutionLoader = new ConstitutionLoader()
1253
+ }
1254
+ return this._constitutionLoader
1255
+ }
1256
+
1257
+ /** Self-Critique: Lazily-initialized RLAIF-style tool-call safety verification. */
1258
+ getSelfCritique(): SelfCritique {
1259
+ if (!this._selfCritique) {
1260
+ this._selfCritique = new SelfCritique()
1261
+ }
1262
+ return this._selfCritique
1263
+ }
1264
+
1144
1265
  /** Register a tool dynamically (used by MCP auto-registration). */
1145
1266
  registerTool(tool: ToolDefinition): void {
1146
1267
  if (this.tools.has(tool.name)) {