@miphamai/cli 0.23.0 → 0.24.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.
- package/package.json +1 -1
- package/src/agent/agent-context.ts +20 -57
- package/src/agent/agent-experience.ts +8 -0
- package/src/agent/effectiveness-tracker.ts +149 -0
- package/src/agent/experience-rules.ts +188 -0
- package/src/agent/pattern-analyzer.ts +154 -0
- package/src/agent/sub-agent.ts +67 -0
- package/src/agent/types.ts +2 -0
- package/src/config/defaults.ts +6 -0
- package/src/core/engine.ts +70 -1
- package/src/core/rule-engine.ts +179 -0
- package/src/index.tsx +3 -1
- package/src/shared/types.ts +14 -0
- package/src/tools/agent/agent.ts +7 -1
- package/src/tools/file/edit.ts +78 -15
- package/src/ui/commands.ts +133 -0
package/src/agent/sub-agent.ts
CHANGED
|
@@ -7,6 +7,16 @@ import { getMessageBus } from './message-bus'
|
|
|
7
7
|
import type { HookEngine } from '../core/hooks'
|
|
8
8
|
import type { PermissionSystem } from '../core/permission'
|
|
9
9
|
import { AgentExperience } from './agent-experience'
|
|
10
|
+
import { PatternAnalyzer } from './pattern-analyzer.js'
|
|
11
|
+
import type { ExperienceRuleEngine } from '../core/rule-engine.js'
|
|
12
|
+
|
|
13
|
+
// Singleton instances (created lazily)
|
|
14
|
+
let _patternAnalyzer: PatternAnalyzer | undefined
|
|
15
|
+
|
|
16
|
+
function getPatternAnalyzer(): PatternAnalyzer {
|
|
17
|
+
if (!_patternAnalyzer) _patternAnalyzer = new PatternAnalyzer()
|
|
18
|
+
return _patternAnalyzer
|
|
19
|
+
}
|
|
10
20
|
|
|
11
21
|
const TYPE_SYSTEM_PROMPTS: Record<SubAgentType, string> = {
|
|
12
22
|
general: 'You are a focused sub-agent. Complete the assigned task thoroughly and return results.',
|
|
@@ -32,6 +42,7 @@ export class SubAgent {
|
|
|
32
42
|
private toolRegistry: Map<string, ToolDefinition>,
|
|
33
43
|
private permission?: PermissionSystem,
|
|
34
44
|
private hookEngine?: HookEngine,
|
|
45
|
+
private ruleEngine?: ExperienceRuleEngine,
|
|
35
46
|
) {}
|
|
36
47
|
|
|
37
48
|
/**
|
|
@@ -85,6 +96,24 @@ export class SubAgent {
|
|
|
85
96
|
options.agentDef,
|
|
86
97
|
)
|
|
87
98
|
}
|
|
99
|
+
|
|
100
|
+
// CRSI: trigger pattern analysis after each agent execution
|
|
101
|
+
// Gated by crsi.autoPatternAnalysis feature flag
|
|
102
|
+
if (options.autoPatternAnalysis !== false) {
|
|
103
|
+
try {
|
|
104
|
+
const analyzer = getPatternAnalyzer()
|
|
105
|
+
const agentName = options.agentDef?.name || agentType
|
|
106
|
+
const patterns = analyzer.analyzeAgent(agentName)
|
|
107
|
+
if (patterns.length > 0 && this.ruleEngine) {
|
|
108
|
+
for (const pattern of patterns) {
|
|
109
|
+
const toolRule = analyzer.toToolRule(pattern)
|
|
110
|
+
this.ruleEngine.register(toolRule)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Pattern analysis failure never blocks agent execution
|
|
115
|
+
}
|
|
116
|
+
}
|
|
88
117
|
})
|
|
89
118
|
|
|
90
119
|
return `[background-task:${taskId}]`
|
|
@@ -97,6 +126,25 @@ export class SubAgent {
|
|
|
97
126
|
await this.hookEngine.executeSubagentStop(agentType, description, 'sub-agent', true, result)
|
|
98
127
|
}
|
|
99
128
|
this.logSuccessExperience(agentType, description, result, options.agentDef)
|
|
129
|
+
|
|
130
|
+
// CRSI: trigger pattern analysis after successful sync execution
|
|
131
|
+
// Gated by crsi.autoPatternAnalysis feature flag
|
|
132
|
+
if (options.autoPatternAnalysis !== false) {
|
|
133
|
+
try {
|
|
134
|
+
const analyzer = getPatternAnalyzer()
|
|
135
|
+
const agentName = options.agentDef?.name || agentType
|
|
136
|
+
const patterns = analyzer.analyzeAgent(agentName)
|
|
137
|
+
if (patterns.length > 0 && this.ruleEngine) {
|
|
138
|
+
for (const pattern of patterns) {
|
|
139
|
+
const toolRule = analyzer.toToolRule(pattern)
|
|
140
|
+
this.ruleEngine.register(toolRule)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Pattern analysis failure never blocks agent execution
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
100
148
|
return result
|
|
101
149
|
} catch (err) {
|
|
102
150
|
if (this.hookEngine) {
|
|
@@ -109,6 +157,25 @@ export class SubAgent {
|
|
|
109
157
|
)
|
|
110
158
|
}
|
|
111
159
|
this.logFailureExperience(agentType, description, String(err), options.agentDef)
|
|
160
|
+
|
|
161
|
+
// CRSI: trigger pattern analysis after failed sync execution (failures produce patterns too)
|
|
162
|
+
// Gated by crsi.autoPatternAnalysis feature flag
|
|
163
|
+
if (options.autoPatternAnalysis !== false) {
|
|
164
|
+
try {
|
|
165
|
+
const analyzer = getPatternAnalyzer()
|
|
166
|
+
const agentName = options.agentDef?.name || agentType
|
|
167
|
+
const patterns = analyzer.analyzeAgent(agentName)
|
|
168
|
+
if (patterns.length > 0 && this.ruleEngine) {
|
|
169
|
+
for (const pattern of patterns) {
|
|
170
|
+
const toolRule = analyzer.toToolRule(pattern)
|
|
171
|
+
this.ruleEngine.register(toolRule)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Pattern analysis failure never blocks agent execution
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
112
179
|
throw err
|
|
113
180
|
}
|
|
114
181
|
}
|
package/src/agent/types.ts
CHANGED
|
@@ -46,4 +46,6 @@ export interface SubAgentOptions {
|
|
|
46
46
|
onProgress?: (chunk: string) => void
|
|
47
47
|
/** When set, tool executions use this path as cwd (git worktree isolation). */
|
|
48
48
|
worktreePath?: string
|
|
49
|
+
/** CRSI: when false, skip pattern analysis after agent execution. Default true. */
|
|
50
|
+
autoPatternAnalysis?: boolean
|
|
49
51
|
}
|
package/src/config/defaults.ts
CHANGED
|
@@ -22,6 +22,12 @@ export const DEFAULT_CONFIG: MiphamConfig = {
|
|
|
22
22
|
mcp: { oauthEnabled: true },
|
|
23
23
|
context: { useRealTokenizer: true, adaptiveThresholds: true },
|
|
24
24
|
},
|
|
25
|
+
crsi: {
|
|
26
|
+
ruleInjection: true,
|
|
27
|
+
preToolHook: true,
|
|
28
|
+
autoPatternAnalysis: true,
|
|
29
|
+
autoRuleManagement: true,
|
|
30
|
+
},
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export const DEFAULT_INFERENCE_HOOK_CONFIG: InferenceHookConfig = {
|
package/src/core/engine.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
|
3
3
|
ToolDefinition,
|
|
4
4
|
ToolResult,
|
|
5
5
|
InferenceHookConfig,
|
|
6
|
+
CrsiConfig,
|
|
6
7
|
} from '../shared/index.ts'
|
|
7
8
|
import { ProviderRegistry } from '../providers/registry'
|
|
8
9
|
import { ContextManager } from './context'
|
|
@@ -16,6 +17,9 @@ import type { AgentViewManager } from '../agent-view/agent-view-manager'
|
|
|
16
17
|
import type { SkillsLoader } from '../skills/loader'
|
|
17
18
|
import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
18
19
|
import { RulesLoader } from './rules-loader'
|
|
20
|
+
import { ExperienceRuleEngine } from './rule-engine.js'
|
|
21
|
+
import { PatternAnalyzer } from '../agent/pattern-analyzer.js'
|
|
22
|
+
import { EffectivenessTracker } from '../agent/effectiveness-tracker.js'
|
|
19
23
|
import { UsageTracker } from './usage-tracker'
|
|
20
24
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
21
25
|
import { getFileInboxTransport } from '../agent/cross-session/file-inbox'
|
|
@@ -37,6 +41,9 @@ export class QueryEngine {
|
|
|
37
41
|
private agentRegistry?: AgentRegistry
|
|
38
42
|
private agentViewManager?: AgentViewManager
|
|
39
43
|
private skillsLoader?: SkillsLoader
|
|
44
|
+
private ruleEngine?: ExperienceRuleEngine
|
|
45
|
+
private _patternAnalyzer?: PatternAnalyzer
|
|
46
|
+
private _effectivenessTracker?: EffectivenessTracker
|
|
40
47
|
private goal?: string
|
|
41
48
|
private maxGoalLoops = 20
|
|
42
49
|
private lastAssistantContent?: string
|
|
@@ -54,7 +61,10 @@ export class QueryEngine {
|
|
|
54
61
|
private context: ContextManager,
|
|
55
62
|
private tools: Map<string, ToolDefinition>,
|
|
56
63
|
private permission: PermissionSystem = new PermissionSystem('default'),
|
|
57
|
-
|
|
64
|
+
ruleEngine?: ExperienceRuleEngine,
|
|
65
|
+
) {
|
|
66
|
+
this.ruleEngine = ruleEngine
|
|
67
|
+
}
|
|
58
68
|
|
|
59
69
|
/** Session identifier for cross-session messaging. Set by the app startup via setSessionId(). */
|
|
60
70
|
private sessionId: string = 'session-1'
|
|
@@ -128,6 +138,8 @@ export class QueryEngine {
|
|
|
128
138
|
private usageTracker = new UsageTracker()
|
|
129
139
|
/** Inference hook (DLP) configuration. */
|
|
130
140
|
private inferenceHookConfig?: InferenceHookConfig
|
|
141
|
+
/** CRSI feature flags. */
|
|
142
|
+
private crsiConfig?: Partial<CrsiConfig>
|
|
131
143
|
|
|
132
144
|
/** Register the rules loader. */
|
|
133
145
|
setRulesLoader(loader: RulesLoader): void {
|
|
@@ -140,6 +152,11 @@ export class QueryEngine {
|
|
|
140
152
|
this.inferenceHookConfig = config
|
|
141
153
|
}
|
|
142
154
|
|
|
155
|
+
/** Register CRSI feature flags. */
|
|
156
|
+
setCrsiConfig(config: Partial<CrsiConfig> | undefined): void {
|
|
157
|
+
this.crsiConfig = config
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
/** Pending task notifications from background agents (cleared after draining). */
|
|
144
161
|
private pendingTaskNotifications: Array<StreamChunk> = []
|
|
145
162
|
|
|
@@ -796,6 +813,7 @@ export class QueryEngine {
|
|
|
796
813
|
|
|
797
814
|
// Run PreToolUse hooks
|
|
798
815
|
let effectiveParams = params
|
|
816
|
+
let hookWarnings: string[] = []
|
|
799
817
|
if (this.hookEngine) {
|
|
800
818
|
const preResult = await this.hookEngine.executePreToolUse(name, params, this.sessionId)
|
|
801
819
|
if (!preResult.allowed) {
|
|
@@ -810,6 +828,17 @@ export class QueryEngine {
|
|
|
810
828
|
}
|
|
811
829
|
}
|
|
812
830
|
|
|
831
|
+
// CRSI RuleEngine intercept — gated by crsi.preToolHook feature flag
|
|
832
|
+
if (this.crsiConfig?.preToolHook !== false && this.ruleEngine) {
|
|
833
|
+
const ruleResult = this.ruleEngine.intercept(name, effectiveParams)
|
|
834
|
+
if (Object.keys(ruleResult.modified).length > 0) {
|
|
835
|
+
effectiveParams = ruleResult.modified
|
|
836
|
+
}
|
|
837
|
+
if (ruleResult.warnings.length > 0) {
|
|
838
|
+
hookWarnings = ruleResult.warnings
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
813
842
|
try {
|
|
814
843
|
const result = await tool.execute(effectiveParams, {
|
|
815
844
|
cwd: process.cwd(),
|
|
@@ -823,16 +852,35 @@ export class QueryEngine {
|
|
|
823
852
|
agentRegistry: this.agentRegistry,
|
|
824
853
|
backgroundAgentRegistry: getBackgroundAgentRegistry(),
|
|
825
854
|
permissionSystem: this.permission,
|
|
855
|
+
ruleEngine: this.ruleEngine,
|
|
826
856
|
})
|
|
827
857
|
|
|
828
858
|
// Track touched files for rules matching
|
|
829
859
|
this.trackTouchedFile(name, effectiveParams)
|
|
830
860
|
|
|
861
|
+
// Prepend CRSI warnings to result content
|
|
862
|
+
if (hookWarnings.length > 0 && result.success) {
|
|
863
|
+
result.content = hookWarnings.join('\n') + '\n' + (result.content || '')
|
|
864
|
+
}
|
|
865
|
+
|
|
831
866
|
// Run PostToolUse hooks
|
|
832
867
|
if (this.hookEngine) {
|
|
833
868
|
await this.hookEngine.executePostToolUse(name, effectiveParams, result, this.sessionId)
|
|
834
869
|
}
|
|
835
870
|
|
|
871
|
+
// CRSI: Track rule effectiveness after tool execution
|
|
872
|
+
if (hookWarnings.length > 0) {
|
|
873
|
+
const tracker = this.getEffectivenessTracker()
|
|
874
|
+
for (const warning of hookWarnings) {
|
|
875
|
+
const match = warning.match(/^\[rule:([^\]]+)\]/)
|
|
876
|
+
const ruleId = match?.[1]
|
|
877
|
+
if (ruleId) {
|
|
878
|
+
tracker.recordApplication(ruleId, result.success)
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
tracker.persist()
|
|
882
|
+
}
|
|
883
|
+
|
|
836
884
|
return result
|
|
837
885
|
} catch (err) {
|
|
838
886
|
// Sanitize error: strip stack traces and internal paths to prevent
|
|
@@ -864,6 +912,27 @@ export class QueryEngine {
|
|
|
864
912
|
return this.tools
|
|
865
913
|
}
|
|
866
914
|
|
|
915
|
+
getRuleEngine(): ExperienceRuleEngine | undefined {
|
|
916
|
+
return this.ruleEngine
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Lazily-initialized PatternAnalyzer singleton. */
|
|
920
|
+
getPatternAnalyzer(): PatternAnalyzer {
|
|
921
|
+
if (!this._patternAnalyzer) {
|
|
922
|
+
this._patternAnalyzer = new PatternAnalyzer()
|
|
923
|
+
}
|
|
924
|
+
return this._patternAnalyzer
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
/** Lazily-initialized EffectivenessTracker singleton. */
|
|
928
|
+
getEffectivenessTracker(): EffectivenessTracker {
|
|
929
|
+
if (!this._effectivenessTracker) {
|
|
930
|
+
this._effectivenessTracker = new EffectivenessTracker()
|
|
931
|
+
this._effectivenessTracker.load()
|
|
932
|
+
}
|
|
933
|
+
return this._effectivenessTracker
|
|
934
|
+
}
|
|
935
|
+
|
|
867
936
|
/** Register a tool dynamically (used by MCP auto-registration). */
|
|
868
937
|
registerTool(tool: ToolDefinition): void {
|
|
869
938
|
if (this.tools.has(tool.name)) {
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { ExperienceRule } from '../agent/experience-rules.js'
|
|
2
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs'
|
|
3
|
+
import { join, dirname } from 'node:path'
|
|
4
|
+
|
|
5
|
+
export interface ToolRule {
|
|
6
|
+
id: string
|
|
7
|
+
toolName: string
|
|
8
|
+
category: string
|
|
9
|
+
match: (params: Record<string, unknown>) => boolean
|
|
10
|
+
fix: (params: Record<string, unknown>) => {
|
|
11
|
+
modified: Record<string, unknown>
|
|
12
|
+
warning: string
|
|
13
|
+
}
|
|
14
|
+
source: 'builtin' | 'pattern-analyzer' | 'manual'
|
|
15
|
+
enabled: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const BUILTIN_RULES: ToolRule[] = [
|
|
19
|
+
{
|
|
20
|
+
id: 'rule-timeout-bash-heavy',
|
|
21
|
+
toolName: 'Bash',
|
|
22
|
+
category: 'timeout',
|
|
23
|
+
match: (p: Record<string, unknown>) => {
|
|
24
|
+
const cmd = String(p.command ?? '')
|
|
25
|
+
const heavy = /npm (install|ci|test)|docker build|pnpm install|cargo build|brew install/.test(
|
|
26
|
+
cmd,
|
|
27
|
+
)
|
|
28
|
+
if (!heavy) return false
|
|
29
|
+
const timeout = (p as Record<string, unknown>).timeout as number | undefined
|
|
30
|
+
return !timeout || timeout < 300_000
|
|
31
|
+
},
|
|
32
|
+
fix: (p: Record<string, unknown>) => {
|
|
33
|
+
const prevTimeout = (p as Record<string, unknown>).timeout as number | undefined
|
|
34
|
+
return {
|
|
35
|
+
modified: { ...p, timeout: 300_000 },
|
|
36
|
+
warning: `⏱️ timeout 已从 ${prevTimeout || 'default'}ms 自动提升至 300000ms(该命令类型历史超时率 > 50%)`,
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
source: 'builtin',
|
|
40
|
+
enabled: true,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'rule-git-force-protection',
|
|
44
|
+
toolName: 'Bash',
|
|
45
|
+
category: 'tool-params',
|
|
46
|
+
match: (p: Record<string, unknown>) => {
|
|
47
|
+
const cmd = String(p.command ?? '')
|
|
48
|
+
return /git (push|reset) .*--force/.test(cmd) && !p.dangerouslyDisableSandbox
|
|
49
|
+
},
|
|
50
|
+
fix: (p: Record<string, unknown>) => ({
|
|
51
|
+
modified: p,
|
|
52
|
+
warning: '⚠️ 检测到 git --force 操作。如需执行请设置 dangerouslyDisableSandbox: true',
|
|
53
|
+
}),
|
|
54
|
+
source: 'builtin',
|
|
55
|
+
enabled: true,
|
|
56
|
+
},
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
export class ExperienceRuleEngine {
|
|
60
|
+
private rules: ToolRule[]
|
|
61
|
+
private storePath: string
|
|
62
|
+
|
|
63
|
+
constructor(storeDir: string = join(process.env.HOME || '~', '.mipham', 'rule-engine')) {
|
|
64
|
+
this.rules = [...BUILTIN_RULES.map((r) => ({ ...r }))]
|
|
65
|
+
this.storePath = join(storeDir, 'rules.json')
|
|
66
|
+
this.load()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
register(rule: ToolRule): void {
|
|
70
|
+
// Replace if same ID exists, otherwise append
|
|
71
|
+
const idx = this.rules.findIndex((r) => r.id === rule.id)
|
|
72
|
+
if (idx !== -1) {
|
|
73
|
+
this.rules[idx] = rule
|
|
74
|
+
} else {
|
|
75
|
+
this.rules.push(rule)
|
|
76
|
+
}
|
|
77
|
+
this.persist()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
intercept(
|
|
81
|
+
toolName: string,
|
|
82
|
+
params: Record<string, unknown>,
|
|
83
|
+
): { modified: Record<string, unknown>; warnings: string[] } {
|
|
84
|
+
let modified = params
|
|
85
|
+
const warnings: string[] = []
|
|
86
|
+
|
|
87
|
+
for (const rule of this.rules) {
|
|
88
|
+
if (!rule.enabled) continue
|
|
89
|
+
if (rule.toolName !== toolName) continue
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
if (rule.match(modified)) {
|
|
93
|
+
const result = rule.fix(modified)
|
|
94
|
+
modified = result.modified
|
|
95
|
+
if (result.warning) {
|
|
96
|
+
warnings.push(`[rule:${rule.id}] ${result.warning}`)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} catch {
|
|
100
|
+
// Rule match/fix failures never block execution
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { modified, warnings }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
convertFromExperienceRules(experienceRules: ExperienceRule[]): ToolRule[] {
|
|
108
|
+
return experienceRules.map((er): ToolRule => {
|
|
109
|
+
// Map experience rule category to tool name
|
|
110
|
+
const toolNameMap: Record<string, string> = {
|
|
111
|
+
timeout: 'Bash',
|
|
112
|
+
'tool-params': 'Bash',
|
|
113
|
+
import: 'Write',
|
|
114
|
+
search: 'Grep',
|
|
115
|
+
semantic: 'Bash',
|
|
116
|
+
}
|
|
117
|
+
const toolName = toolNameMap[er.category] || 'Bash'
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
id: er.id,
|
|
121
|
+
toolName,
|
|
122
|
+
category: er.category,
|
|
123
|
+
match: (p: Record<string, unknown>): boolean => {
|
|
124
|
+
if (er.category === 'timeout') {
|
|
125
|
+
const cmd = String(p.command ?? '')
|
|
126
|
+
const heavy = /npm|docker|pnpm|cargo|brew|install|build/.test(cmd)
|
|
127
|
+
if (!heavy) return false
|
|
128
|
+
const timeout = (p as Record<string, unknown>).timeout as number | undefined
|
|
129
|
+
return !timeout || timeout < 300_000
|
|
130
|
+
}
|
|
131
|
+
// Default: always match for the given tool
|
|
132
|
+
return true
|
|
133
|
+
},
|
|
134
|
+
fix: (p: Record<string, unknown>) => ({
|
|
135
|
+
modified: { ...p, timeout: 300_000 },
|
|
136
|
+
warning: `⏱️ [auto-rule] ${er.action} (${er.evidence.failureCount} 次历史失败)`,
|
|
137
|
+
}),
|
|
138
|
+
source: 'pattern-analyzer',
|
|
139
|
+
enabled: true,
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
getActiveRules(): ToolRule[] {
|
|
145
|
+
return this.rules.filter((r) => r.enabled)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
setRuleEnabled(id: string, enabled: boolean): void {
|
|
149
|
+
const rule = this.rules.find((r) => r.id === id)
|
|
150
|
+
if (rule) {
|
|
151
|
+
rule.enabled = enabled
|
|
152
|
+
this.persist()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Persist non-builtin rules to disk. */
|
|
157
|
+
persist(): void {
|
|
158
|
+
const nonBuiltin = this.rules.filter((r) => r.source !== 'builtin')
|
|
159
|
+
const dir = dirname(this.storePath)
|
|
160
|
+
mkdirSync(dir, { recursive: true })
|
|
161
|
+
writeFileSync(this.storePath, JSON.stringify(nonBuiltin, null, 2), 'utf-8')
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Load persisted non-builtin rules from disk. Rejects rules whose IDs conflict with builtins. */
|
|
165
|
+
load(): void {
|
|
166
|
+
if (!existsSync(this.storePath)) return
|
|
167
|
+
try {
|
|
168
|
+
const raw = JSON.parse(readFileSync(this.storePath, 'utf-8')) as ToolRule[]
|
|
169
|
+
const builtinIds = new Set(BUILTIN_RULES.map((r) => r.id))
|
|
170
|
+
for (const rule of raw) {
|
|
171
|
+
// Reject if a builtin with the same ID exists (builtins always win)
|
|
172
|
+
if (builtinIds.has(rule.id)) continue
|
|
173
|
+
this.rules.push(rule)
|
|
174
|
+
}
|
|
175
|
+
} catch {
|
|
176
|
+
// Corrupt file — start fresh with only builtins
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -20,6 +20,7 @@ import { InstructionsLoader } from './core/instructions'
|
|
|
20
20
|
import { loadSessionMemories, getMemoryManager } from './core/memory/memory-loader'
|
|
21
21
|
import { ContextManager } from './core/context'
|
|
22
22
|
import { QueryEngine } from './core/engine'
|
|
23
|
+
import { ExperienceRuleEngine } from './core/rule-engine.js'
|
|
23
24
|
import { SessionStore } from './core/session-store'
|
|
24
25
|
import type { PermissionLevel } from './shared/types'
|
|
25
26
|
import { SkillsLoader } from './skills/loader'
|
|
@@ -247,7 +248,8 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
247
248
|
const artifactServer = new ArtifactServer(artifactsDir, ARTIFACT_PORT)
|
|
248
249
|
|
|
249
250
|
// Create query engine
|
|
250
|
-
const
|
|
251
|
+
const ruleEngine = new ExperienceRuleEngine()
|
|
252
|
+
const engine = new QueryEngine(registry, context, tools, undefined, ruleEngine)
|
|
251
253
|
engine.setHookEngine(hookEngine)
|
|
252
254
|
engine.setArtifactServer(artifactServer)
|
|
253
255
|
engine.setAgentViewManager(agentViewManager)
|
package/src/shared/types.ts
CHANGED
|
@@ -72,6 +72,7 @@ export interface ToolContext {
|
|
|
72
72
|
agentRegistry?: import('../agent/agent-registry').AgentRegistry
|
|
73
73
|
backgroundAgentRegistry?: import('../agent/background-registry').BackgroundAgentRegistry
|
|
74
74
|
permissionSystem?: import('../core/permission').PermissionSystem
|
|
75
|
+
ruleEngine?: import('../core/rule-engine').ExperienceRuleEngine
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// ── Artifact Types ──
|
|
@@ -162,6 +163,8 @@ export interface MiphamConfig {
|
|
|
162
163
|
}
|
|
163
164
|
/** Phase 9 feature flags. All default to true. */
|
|
164
165
|
features?: Partial<FeatureFlags>
|
|
166
|
+
/** Phase 10 CRSI feature flags. All default to true. */
|
|
167
|
+
crsi?: Partial<CrsiConfig>
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
export interface FeatureFlags {
|
|
@@ -169,6 +172,17 @@ export interface FeatureFlags {
|
|
|
169
172
|
context: { useRealTokenizer: boolean; adaptiveThresholds: boolean }
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
export interface CrsiConfig {
|
|
176
|
+
/** Extract and inject experience rules into agent system prompts. */
|
|
177
|
+
ruleInjection: boolean
|
|
178
|
+
/** Intercept tool calls and auto-fix known failure patterns before execution. */
|
|
179
|
+
preToolHook: boolean
|
|
180
|
+
/** Analyze agent outcomes for recurring failure patterns. */
|
|
181
|
+
autoPatternAnalysis: boolean
|
|
182
|
+
/** Auto-degrade/disable low-effectiveness rules based on success-rate tracking. */
|
|
183
|
+
autoRuleManagement: boolean
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
export interface McpServerConfig {
|
|
173
187
|
name: string
|
|
174
188
|
command: string
|
package/src/tools/agent/agent.ts
CHANGED
|
@@ -60,7 +60,13 @@ export const agentTool: ToolDefinition = {
|
|
|
60
60
|
const agentDef = ctx.agentRegistry?.resolve(agentType)
|
|
61
61
|
|
|
62
62
|
try {
|
|
63
|
-
const sub = new SubAgent(
|
|
63
|
+
const sub = new SubAgent(
|
|
64
|
+
registry,
|
|
65
|
+
toolRegistry,
|
|
66
|
+
ctx.permissionSystem,
|
|
67
|
+
undefined,
|
|
68
|
+
ctx.ruleEngine,
|
|
69
|
+
)
|
|
64
70
|
const result = await sub.execute(prompt, description, {
|
|
65
71
|
type: agentType,
|
|
66
72
|
agentDef,
|
package/src/tools/file/edit.ts
CHANGED
|
@@ -2,10 +2,65 @@ import { readFileSync, writeFileSync } from 'node:fs'
|
|
|
2
2
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
3
3
|
import { resolveSafe } from '../../security/path'
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Characters considered part of an identifier in source code.
|
|
7
|
+
* A match is only valid if it sits at identifier boundaries —
|
|
8
|
+
* i.e. not embedded inside a larger identifier word.
|
|
9
|
+
*/
|
|
10
|
+
function isIdentifierChar(c: string): boolean {
|
|
11
|
+
return /[a-zA-Z0-9_$]/.test(c)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find all valid occurrences of `oldStr` in `content`.
|
|
16
|
+
* A match is valid only when it does NOT sit inside a larger identifier:
|
|
17
|
+
* - If oldStr starts with an identifier char, the char before the match
|
|
18
|
+
* must NOT be an identifier char (or the match must be at position 0).
|
|
19
|
+
* - If oldStr ends with an identifier char, the char after the match
|
|
20
|
+
* must NOT be an identifier char (or the match must be at end of file).
|
|
21
|
+
*/
|
|
22
|
+
function findValidMatches(content: string, oldStr: string): number[] {
|
|
23
|
+
const indices: number[] = []
|
|
24
|
+
// oldStr is guaranteed non-empty by the required parameter validation
|
|
25
|
+
const oldFirst = oldStr[0]!
|
|
26
|
+
const oldLast = oldStr[oldStr.length - 1]!
|
|
27
|
+
const checkLeft = isIdentifierChar(oldFirst)
|
|
28
|
+
const checkRight = isIdentifierChar(oldLast)
|
|
29
|
+
|
|
30
|
+
let pos = 0
|
|
31
|
+
while (pos < content.length) {
|
|
32
|
+
const idx = content.indexOf(oldStr, pos)
|
|
33
|
+
if (idx === -1) break
|
|
34
|
+
|
|
35
|
+
// Left boundary check: if oldStr starts with identifier char,
|
|
36
|
+
// the character before the match must not be an identifier char.
|
|
37
|
+
if (checkLeft && idx > 0 && isIdentifierChar(content[idx - 1]!)) {
|
|
38
|
+
pos = idx + 1
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Right boundary check: if oldStr ends with identifier char,
|
|
43
|
+
// the character after the match must not be an identifier char.
|
|
44
|
+
const afterIdx = idx + oldStr.length
|
|
45
|
+
if (checkRight && afterIdx < content.length && isIdentifierChar(content[afterIdx]!)) {
|
|
46
|
+
pos = idx + 1
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
indices.push(idx)
|
|
51
|
+
pos = idx + 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return indices
|
|
55
|
+
}
|
|
56
|
+
|
|
5
57
|
export const editTool: ToolDefinition = {
|
|
6
58
|
name: 'Edit',
|
|
7
59
|
description:
|
|
8
|
-
'Perform exact string replacement in a file.
|
|
60
|
+
'Perform exact string replacement in a file. ' +
|
|
61
|
+
'old_string must match exactly as a standalone occurrence — it will NOT match inside larger ' +
|
|
62
|
+
'identifiers or words (e.g. "user" will not match inside "username"). ' +
|
|
63
|
+
'Must be unique in the file (unless replace_all is used).',
|
|
9
64
|
category: 'file',
|
|
10
65
|
permission: 'ask',
|
|
11
66
|
parameters: {
|
|
@@ -43,22 +98,30 @@ export const editTool: ToolDefinition = {
|
|
|
43
98
|
}
|
|
44
99
|
}
|
|
45
100
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
const matches = findValidMatches(content, oldStr)
|
|
102
|
+
|
|
103
|
+
if (matches.length === 0) {
|
|
104
|
+
return {
|
|
105
|
+
success: false,
|
|
106
|
+
content: '',
|
|
107
|
+
error:
|
|
108
|
+
'old_string not found in file. ' +
|
|
109
|
+
'It may appear only as part of a larger word/identifier — provide more context.',
|
|
49
110
|
}
|
|
50
|
-
const updated = content.replaceAll(oldStr, newStr)
|
|
51
|
-
writeFileSync(filePath, updated, 'utf-8')
|
|
52
|
-
const count = content.split(oldStr).length - 1
|
|
53
|
-
return { success: true, content: `Replaced ${count} occurrences in ${filePath}` }
|
|
54
111
|
}
|
|
55
112
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
113
|
+
if (replaceAll) {
|
|
114
|
+
// Build result by splicing at each match (in reverse so indices stay valid)
|
|
115
|
+
let result = content
|
|
116
|
+
for (let i = matches.length - 1; i >= 0; i--) {
|
|
117
|
+
const idx = matches[i]!
|
|
118
|
+
result = result.slice(0, idx) + newStr + result.slice(idx + oldStr.length)
|
|
119
|
+
}
|
|
120
|
+
writeFileSync(filePath, result, 'utf-8')
|
|
121
|
+
return { success: true, content: `Replaced ${matches.length} occurrences in ${filePath}` }
|
|
59
122
|
}
|
|
60
|
-
|
|
61
|
-
if (
|
|
123
|
+
|
|
124
|
+
if (matches.length > 1) {
|
|
62
125
|
return {
|
|
63
126
|
success: false,
|
|
64
127
|
content: '',
|
|
@@ -66,8 +129,8 @@ export const editTool: ToolDefinition = {
|
|
|
66
129
|
}
|
|
67
130
|
}
|
|
68
131
|
|
|
69
|
-
const
|
|
70
|
-
|
|
132
|
+
const idx = matches[0]!
|
|
133
|
+
const updated = content.slice(0, idx) + newStr + content.slice(idx + oldStr.length)
|
|
71
134
|
writeFileSync(filePath, updated, 'utf-8')
|
|
72
135
|
return { success: true, content: `Replaced 1 occurrence in ${filePath}` }
|
|
73
136
|
},
|