thatgfsj-code 0.2.1 → 0.2.2

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +131 -0
  2. package/DEVELOPMENT.md +286 -0
  3. package/README.md +131 -30
  4. package/dist/core/ai-engine.d.ts +17 -0
  5. package/dist/core/ai-engine.d.ts.map +1 -1
  6. package/dist/core/ai-engine.js +23 -7
  7. package/dist/core/ai-engine.js.map +1 -1
  8. package/dist/index.js +18 -27
  9. package/dist/index.js.map +1 -1
  10. package/dist/repl/input.d.ts +8 -0
  11. package/dist/repl/input.d.ts.map +1 -1
  12. package/dist/repl/input.js +12 -0
  13. package/dist/repl/input.js.map +1 -1
  14. package/dist/repl/loop.d.ts +56 -0
  15. package/dist/repl/loop.d.ts.map +1 -1
  16. package/dist/repl/loop.js +333 -4
  17. package/dist/repl/loop.js.map +1 -1
  18. package/dist/repl/output.d.ts.map +1 -1
  19. package/dist/repl/output.js +3 -1
  20. package/dist/repl/output.js.map +1 -1
  21. package/dist/repl/welcome.js +1 -1
  22. package/dist/repl/welcome.js.map +1 -1
  23. package/docs/API_KEY_GUIDE.md +6 -0
  24. package/docs/FAQ.md +25 -3
  25. package/package.json +35 -3
  26. package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +0 -279
  27. package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +0 -564
  28. package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +0 -194
  29. package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +0 -24
  30. package/ROADMAP.md +0 -107
  31. package/src/agent/core.ts +0 -179
  32. package/src/agent/index.ts +0 -8
  33. package/src/agent/intent.ts +0 -181
  34. package/src/agent/streaming.ts +0 -132
  35. package/src/core/ai-engine.ts +0 -437
  36. package/src/core/cli.ts +0 -171
  37. package/src/core/config.ts +0 -147
  38. package/src/core/context-compactor.ts +0 -245
  39. package/src/core/hooks.ts +0 -196
  40. package/src/core/permissions.ts +0 -308
  41. package/src/core/session.ts +0 -165
  42. package/src/core/skills.ts +0 -208
  43. package/src/core/state.ts +0 -120
  44. package/src/core/subagent.ts +0 -195
  45. package/src/core/system-prompt.ts +0 -163
  46. package/src/core/tool-registry.ts +0 -157
  47. package/src/core/types.ts +0 -280
  48. package/src/index.ts +0 -544
  49. package/src/mcp/client.ts +0 -330
  50. package/src/repl/index.ts +0 -8
  51. package/src/repl/input.ts +0 -139
  52. package/src/repl/loop.ts +0 -280
  53. package/src/repl/output.ts +0 -222
  54. package/src/repl/welcome.ts +0 -296
  55. package/src/tools/file.ts +0 -117
  56. package/src/tools/git.ts +0 -132
  57. package/src/tools/index.ts +0 -48
  58. package/src/tools/search.ts +0 -263
  59. package/src/tools/shell.ts +0 -181
  60. package/src/utils/diff-preview.ts +0 -202
  61. package/src/utils/index.ts +0 -8
  62. package/src/utils/memory.ts +0 -223
  63. package/src/utils/project-context.ts +0 -207
  64. package/tsconfig.json +0 -19
package/src/core/hooks.ts DELETED
@@ -1,196 +0,0 @@
1
- /**
2
- * S08: Hooks System
3
- * Inject custom logic at key event points without modifying agent source code
4
- *
5
- * Hook points: before/after tool calls, before/after agent loop, onError, etc.
6
- * Users can register hooks for: audit logging, auto-formatting, security filtering, notifications
7
- */
8
-
9
- import { ChatMessage, ToolCall } from './types.js';
10
-
11
- // ==================== Hook Types ====================
12
-
13
- export type HookName =
14
- | 'beforeToolCall'
15
- | 'afterToolCall'
16
- | 'beforeAgentLoop'
17
- | 'afterAgentLoop'
18
- | 'onError'
19
- | 'beforeMessage'
20
- | 'afterMessage'
21
- | 'onCompact';
22
-
23
- export interface HookContext {
24
- toolName?: string;
25
- toolParams?: Record<string, any>;
26
- toolResult?: any;
27
- messages?: ChatMessage[];
28
- error?: Error;
29
- iteration?: number;
30
- [key: string]: any;
31
- }
32
-
33
- export type HookFn = (ctx: HookContext) => void | Promise<void>;
34
-
35
- export interface HookRegistration {
36
- name: HookName;
37
- fn: HookFn;
38
- priority?: number; // Higher = runs first
39
- once?: boolean; // If true, auto-remove after first run
40
- }
41
-
42
- // ==================== Hook Manager ====================
43
-
44
- export class HookManager {
45
- private hooks: Map<HookName, HookRegistration[]> = new Map();
46
-
47
- constructor() {
48
- // Initialize all hook types
49
- const allHooks: HookName[] = [
50
- 'beforeToolCall', 'afterToolCall', 'beforeAgentLoop', 'afterAgentLoop',
51
- 'onError', 'beforeMessage', 'afterMessage', 'onCompact'
52
- ];
53
- for (const h of allHooks) {
54
- this.hooks.set(h, []);
55
- }
56
- }
57
-
58
- /**
59
- * S08: Register a hook
60
- * @param name Hook point name
61
- * @param fn Function to run
62
- * @param priority Higher priority runs first
63
- * @param once If true, auto-removed after first run
64
- */
65
- register(name: HookName, fn: HookFn, priority = 0, once = false): void {
66
- const regs = this.hooks.get(name) || [];
67
- regs.push({ name, fn, priority, once });
68
- regs.sort((a, b) => (b.priority || 0) - (a.priority || 0));
69
- this.hooks.set(name, regs);
70
- }
71
-
72
- /**
73
- * S08: Unregister a hook by function reference
74
- */
75
- unregister(name: HookName, fn: HookFn): void {
76
- const regs = this.hooks.get(name) || [];
77
- this.hooks.set(name, regs.filter(r => r.fn !== fn));
78
- }
79
-
80
- /**
81
- * S08: Clear all hooks for a given name (or all)
82
- */
83
- clear(name?: HookName): void {
84
- if (name) {
85
- this.hooks.set(name, []);
86
- } else {
87
- for (const [, regs] of this.hooks) {
88
- regs.length = 0;
89
- }
90
- }
91
- }
92
-
93
- /**
94
- * S08: Emit a hook — call all registered functions at that point
95
- * Returns after all sync hooks complete, awaits async ones
96
- */
97
- async emit(name: HookName, ctx: HookContext): Promise<void> {
98
- const regs = this.hooks.get(name) || [];
99
-
100
- for (const reg of regs) {
101
- try {
102
- await Promise.resolve(reg.fn(ctx));
103
- } catch (error: any) {
104
- console.error(`[Hook ${name}] error:`, error.message);
105
- }
106
-
107
- // Remove if one-time hook
108
- if (reg.once) {
109
- this.unregister(name, reg.fn);
110
- }
111
- }
112
- }
113
-
114
- /**
115
- * S08: Check if any hooks are registered for a given name
116
- */
117
- hasHooks(name: HookName): boolean {
118
- return (this.hooks.get(name)?.length || 0) > 0;
119
- }
120
-
121
- /**
122
- * S08: Get hook count
123
- */
124
- getHookCount(name?: HookName): number {
125
- if (name) {
126
- return this.hooks.get(name)?.length || 0;
127
- }
128
- let total = 0;
129
- for (const regs of this.hooks.values()) {
130
- total += regs.length;
131
- }
132
- return total;
133
- }
134
- }
135
-
136
- // ==================== Built-in Hooks ====================
137
-
138
- /**
139
- * S08: Audit logging hook — logs all tool calls
140
- */
141
- export function auditLogHook(ctx: HookContext): void {
142
- if (ctx.toolName) {
143
- const ts = new Date().toISOString().split('T')[1].split('.')[0];
144
- const status = ctx.toolResult?.success ? 'OK' : 'FAIL';
145
- console.log(`[AUDIT ${ts}] ${status} | tool=${ctx.toolName} | params=${JSON.stringify(ctx.toolParams || {})} | result=${ctx.toolResult?.success ? 'OK' : ctx.toolResult?.error}`);
146
- }
147
- }
148
-
149
- /**
150
- * S08: Prettier auto-format hook — run prettier after file writes
151
- */
152
- export async function prettierFormatHook(ctx: HookContext): Promise<void> {
153
- if (ctx.toolName === 'file' && ctx.toolParams?.action === 'write') {
154
- const path = ctx.toolParams.path;
155
- const ext = path.split('.').pop()?.toLowerCase();
156
-
157
- const formattable = ['js', 'ts', 'jsx', 'tsx', 'json', 'md', 'css', 'html'];
158
- if (formattable.includes(ext || '')) {
159
- console.log(`[Hook] Auto-format skipped (prettier not installed) for ${path}`);
160
- // In real impl: spawn prettier on the file
161
- }
162
- }
163
- }
164
-
165
- /**
166
- * S08: Security filter hook — block suspicious patterns
167
- */
168
- export function securityFilterHook(ctx: HookContext): { blocked: boolean; reason: string } {
169
- if (ctx.toolName === 'shell' && ctx.toolParams?.command) {
170
- const cmd = ctx.toolParams.command;
171
- const suspicious = [
172
- /curl\s+.*\|.*sh/i,
173
- /wget\s+.*\|.*sh/i,
174
- /base64\s+-d/i,
175
- /\.\/[^/]+\s+&&.*rm/i
176
- ];
177
-
178
- for (const pattern of suspicious) {
179
- if (pattern.test(cmd)) {
180
- return { blocked: true, reason: `Suspicious pattern detected: ${pattern}` };
181
- }
182
- }
183
- }
184
- return { blocked: false, reason: '' };
185
- }
186
-
187
- // ==================== Singleton ====================
188
-
189
- let globalHookManager: HookManager | null = null;
190
-
191
- export function getHookManager(): HookManager {
192
- if (!globalHookManager) {
193
- globalHookManager = new HookManager();
194
- }
195
- return globalHookManager;
196
- }
@@ -1,308 +0,0 @@
1
- /**
2
- * S03: Permissions System
3
- * 6-stage permission pipeline for tool execution
4
- * Core principle: permissions are first-class citizens, not an afterthought
5
- */
6
-
7
- import chalk from 'chalk';
8
- import { ToolContext, ToolMetadata } from './types.js';
9
-
10
- // ==================== Permission Types ====================
11
-
12
- export type PermissionMode = 'accept' | 'deny' | 'ask';
13
- export type PermissionLevel = 'none' | 'low' | 'medium' | 'high' | 'critical';
14
-
15
- export interface PermissionResult {
16
- allowed: boolean;
17
- reason: string;
18
- level: PermissionLevel;
19
- requiresConfirmation: boolean;
20
- stage: 1 | 2 | 3 | 4 | 5 | 6;
21
- }
22
-
23
- /**
24
- * Permission rule — pattern-based allow/deny rules
25
- */
26
- export interface PermissionRule {
27
- pattern: RegExp;
28
- action: 'allow' | 'deny';
29
- reason: string;
30
- }
31
-
32
- /**
33
- * S03: Permission Checker — 6-stage pipeline
34
- */
35
- export class PermissionChecker {
36
- private mode: PermissionMode = 'ask';
37
- private rules: PermissionRule[] = [];
38
- private workspaceRoot: string;
39
-
40
- // Stage 1: Blocked patterns
41
- private static BLOCKED_PATTERNS = [
42
- /^rm\s+-rf\s+\//i,
43
- /^rm\s+-rf\s+\$HOME/i,
44
- /^rm\s+-rf\s+%USERPROFILE%/i,
45
- /^del\s+\/f\s+\/s\s+\/q/i,
46
- /^format\s+[a-z]:/i,
47
- /^mkfs/i,
48
- /^dd\s+if=/i,
49
- /^shred/i,
50
- />\s*\/dev\/sda/i,
51
- /^curl\s+.*\||^wget\s+.*\|/i,
52
- /^eval\s+/i,
53
- /^exec\s+/i,
54
- /base64\s+-d\s+.*\|/i,
55
- /\$\(.*\)/i,
56
- /`.*`/i,
57
- ];
58
-
59
- // Stage 2: Commands needing confirmation
60
- private static CONFIRM_REQUIRED = [
61
- 'rm -rf',
62
- 'rmdir',
63
- 'del /s /q',
64
- 'format',
65
- 'mkfs',
66
- 'dd',
67
- 'shutdown',
68
- 'init 0',
69
- 'init 6',
70
- 'poweroff',
71
- 'reboot',
72
- 'pkill',
73
- 'killall',
74
- 'mv .* /tmp',
75
- 'chmod 777',
76
- 'chmod -R 777',
77
- 'git push --force',
78
- 'git push -f',
79
- ];
80
-
81
- // Stage 4: AI borderline commands (use LLM to classify)
82
- private static BORDERLINE_PATTERNS = [
83
- /curl\s+https?:\/\//i,
84
- /wget\s+/i,
85
- /npm\s+install\s+-g/i,
86
- /pip\s+install\s+/i,
87
- /yarn\s+add\s+/i,
88
- /sudo\s+/i,
89
- /chown\s+/i,
90
- /chmod\s+[67]/i,
91
- /^git\s+reset/i,
92
- /^git\s+clean/i,
93
- ];
94
-
95
- constructor(workspaceRoot: string = process.cwd()) {
96
- this.workspaceRoot = workspaceRoot;
97
- }
98
-
99
- /**
100
- * S03: 6-Stage Permission Pipeline
101
- * Every tool execution passes through all 6 stages
102
- */
103
- async check(
104
- toolName: string,
105
- params: Record<string, any>,
106
- ctx: ToolContext
107
- ): Promise<PermissionResult> {
108
-
109
- // Stage 1: Pattern block — reject known dangerous commands immediately
110
- const stage1 = this.stage1PatternBlock(toolName, params);
111
- if (!stage1.allowed) return stage1;
112
-
113
- // Stage 2: Permission rules — check user-defined allow/deny rules
114
- const stage2 = this.stage2Rules(toolName, params);
115
- if (!stage2.allowed) return stage2;
116
-
117
- // Stage 3: Path sandboxing — prevent workspace escape
118
- const stage3 = this.stage3PathSandbox(toolName, params);
119
- if (!stage3.allowed) return stage3;
120
-
121
- // Stage 4: AI classification for borderline cases
122
- const stage4 = await this.stage4AIClassify(toolName, params, ctx);
123
- if (!stage4.allowed) return stage4;
124
-
125
- // Stage 5: User confirmation for medium/high risk
126
- const stage5 = this.stage5Confirmation(toolName, params, ctx);
127
- if (stage5.requiresConfirmation) {
128
- return stage5;
129
- }
130
-
131
- // Stage 6: Pass through
132
- return {
133
- allowed: true,
134
- reason: 'Allowed',
135
- level: stage5.level,
136
- requiresConfirmation: false,
137
- stage: 6
138
- };
139
- }
140
-
141
- // Stage 1: Pattern block
142
- private stage1PatternBlock(toolName: string, params: Record<string, any>): PermissionResult {
143
- if (toolName !== 'shell') return { allowed: true, reason: 'Stage 1: N/A', level: 'none', requiresConfirmation: false, stage: 1 };
144
-
145
- const command = params.command || '';
146
- for (const pattern of PermissionChecker.BLOCKED_PATTERNS) {
147
- if (pattern.test(command.trim())) {
148
- return {
149
- allowed: false,
150
- reason: `Blocked: command matches dangerous pattern ${pattern}`,
151
- level: 'critical',
152
- requiresConfirmation: false,
153
- stage: 1
154
- };
155
- }
156
- }
157
- return { allowed: true, reason: 'Stage 1: Passed pattern check', level: 'none', requiresConfirmation: false, stage: 1 };
158
- }
159
-
160
- // Stage 2: User-defined rules
161
- private stage2Rules(toolName: string, params: Record<string, any>): PermissionResult {
162
- const command = params.command || '';
163
-
164
- for (const rule of this.rules) {
165
- if (rule.pattern.test(command)) {
166
- return {
167
- allowed: rule.action === 'allow',
168
- reason: `Rule matched: ${rule.reason}`,
169
- level: rule.action === 'deny' ? 'high' : 'none',
170
- requiresConfirmation: false,
171
- stage: 2
172
- };
173
- }
174
- }
175
- return { allowed: true, reason: 'Stage 2: No rules matched', level: 'none', requiresConfirmation: false, stage: 2 };
176
- }
177
-
178
- // Stage 3: Path sandboxing
179
- private stage3PathSandbox(toolName: string, params: Record<string, any>): PermissionResult {
180
- if (toolName !== 'shell' && toolName !== 'file') return { allowed: true, reason: 'Stage 3: N/A', level: 'none', requiresConfirmation: false, stage: 3 };
181
-
182
- const path = params.path || params.command || '';
183
-
184
- // Check for path traversal attempts
185
- if (/^\.\.\//.test(path) || /\%00/.test(path) || /\0/.test(path)) {
186
- return {
187
- allowed: false,
188
- reason: 'Blocked: path traversal attempt detected',
189
- level: 'high',
190
- requiresConfirmation: false,
191
- stage: 3
192
- };
193
- }
194
-
195
- return { allowed: true, reason: 'Stage 3: Path sandbox passed', level: 'none', requiresConfirmation: false, stage: 3 };
196
- }
197
-
198
- // Stage 4: AI classification for borderline commands
199
- private async stage4AIClassify(
200
- toolName: string,
201
- params: Record<string, any>,
202
- ctx: ToolContext
203
- ): Promise<PermissionResult> {
204
- if (toolName !== 'shell') return { allowed: true, reason: 'Stage 4: N/A', level: 'none', requiresConfirmation: false, stage: 4 };
205
-
206
- const command = params.command || '';
207
-
208
- // Check if borderline
209
- const isBorderline = PermissionChecker.BORDERLINE_PATTERNS.some(p => p.test(command));
210
- if (!isBorderline) {
211
- return { allowed: true, reason: 'Stage 4: Not borderline', level: 'none', requiresConfirmation: false, stage: 4 };
212
- }
213
-
214
- // For CLI, mark as medium risk (actual AI classification skipped for perf)
215
- return {
216
- allowed: true,
217
- reason: 'Stage 4: Borderline — escalated to confirmation',
218
- level: 'medium',
219
- requiresConfirmation: false,
220
- stage: 4
221
- };
222
- }
223
-
224
- // Stage 5: User confirmation
225
- private stage5Confirmation(
226
- toolName: string,
227
- params: Record<string, any>,
228
- ctx: ToolContext
229
- ): PermissionResult {
230
- if (this.mode === 'accept') {
231
- return { allowed: true, reason: 'Mode: accept all', level: 'none', requiresConfirmation: false, stage: 5 };
232
- }
233
-
234
- if (this.mode === 'deny') {
235
- return { allowed: false, reason: 'Mode: deny all', level: 'high', requiresConfirmation: false, stage: 5 };
236
- }
237
-
238
- // mode === 'ask'
239
- const command = params.command || '';
240
-
241
- // Critical: auto-deny
242
- if (/rm\s+-rf/.test(command) && !command.includes('node_modules')) {
243
- return {
244
- allowed: false,
245
- reason: 'Auto-deny: rm -rf without node_modules is too risky',
246
- level: 'critical',
247
- requiresConfirmation: false,
248
- stage: 5
249
- };
250
- }
251
-
252
- // Check confirm required list
253
- const needsConfirm = PermissionChecker.CONFIRM_REQUIRED.some(cmd =>
254
- command.toLowerCase().includes(cmd.toLowerCase())
255
- );
256
-
257
- if (needsConfirm) {
258
- return {
259
- allowed: true,
260
- reason: 'Requires user confirmation',
261
- level: 'medium',
262
- requiresConfirmation: true,
263
- stage: 5
264
- };
265
- }
266
-
267
- return { allowed: true, reason: 'Stage 5: No confirmation needed', level: 'low', requiresConfirmation: false, stage: 5 };
268
- }
269
-
270
- // ==================== Config Methods ====================
271
-
272
- setMode(mode: PermissionMode) {
273
- this.mode = mode;
274
- }
275
-
276
- getMode(): PermissionMode {
277
- return this.mode;
278
- }
279
-
280
- addRule(pattern: RegExp, action: 'allow' | 'deny', reason: string) {
281
- this.rules.push({ pattern, action, reason });
282
- }
283
-
284
- clearRules() {
285
- this.rules = [];
286
- }
287
-
288
- setWorkspaceRoot(path: string) {
289
- this.workspaceRoot = path;
290
- }
291
- }
292
-
293
- // ==================== Permission Mode CLI ====================
294
-
295
- export function printPermissionResult(result: PermissionResult, command?: string) {
296
- if (!result.allowed) {
297
- console.log(chalk.red(`\n🚫 BLOCKED [Stage ${result.stage}]: ${result.reason}`));
298
- return;
299
- }
300
-
301
- if (result.requiresConfirmation && command) {
302
- console.log(chalk.yellow(`\n⚠️ Requires confirmation [Stage ${result.stage}]: ${result.reason}`));
303
- console.log(chalk.gray(` Command: ${command}`));
304
- return;
305
- }
306
-
307
- console.log(chalk.green(`\n✅ Allowed [Stage ${result.stage}]: ${result.reason}`));
308
- }
@@ -1,165 +0,0 @@
1
- /**
2
- * Session Manager - Manages conversation history
3
- *
4
- * F4 (anti-[已中断]):
5
- * - addMessage 拦截含"已中断" / 链形 think 块等污染字符串
6
- * (之前 AI 在失败重试时会把这些字面写进 history 造成病毒循环)
7
- * - sanitize() 在 lastMessages 输出到 LLM 前再做一次去噪
8
- */
9
-
10
- import { ChatMessage, Session } from './types.js';
11
-
12
- /**
13
- * Patterns that indicate the message is polluted by a previous broken
14
- * tool/stream retry loop. These should never be re-sent to the LLM.
15
- */
16
- const POLLUTION_PATTERNS: RegExp[] = [
17
- /\[已中断/, // literally "[已中断" - sentinel from bad run
18
- /\u5df2\u4e2d\u65ad/, // 已中断 (escaped form)
19
- /^\s*\[已中断\s*$/m, // line that is exactly [已中断
20
- /^<think>[\s\S]*?<\/think>\s*$/m, // entire message that is just a think block
21
- /\u{1F6AB}/u, // ⛔ emoji sometimes used as interrupt marker
22
- ];
23
-
24
- function looksPolluted(content: string): boolean {
25
- if (!content) return false;
26
- // Strip obvious thinking fences and check the *remaining* core content.
27
- const stripped = content
28
- .replace(/<think>[\s\S]*?<\/think>/g, '')
29
- .replace(/<\/?think>/g, '')
30
- .trim();
31
-
32
- for (const p of POLLUTION_PATTERNS) {
33
- if (p.test(content)) return true;
34
- }
35
-
36
- // A message whose entire stripped form is shorter than 6 chars AND
37
- // contains an interruption-related token is also suspect.
38
- if (stripped.length < 6 && /(中断|interrupted|cancelled|已取消)/i.test(stripped)) {
39
- return true;
40
- }
41
- return false;
42
- }
43
-
44
- export class SessionManager {
45
- private messages: ChatMessage[] = [];
46
- private sessionId: string;
47
- private createdAt: Date;
48
- private droppedCount = 0;
49
-
50
- constructor() {
51
- this.sessionId = this.generateId();
52
- this.createdAt = new Date();
53
- }
54
-
55
- /**
56
- * Add a message to the session.
57
- * Returns true if accepted, false if dropped because it looked polluted.
58
- */
59
- addMessage(role: ChatMessage['role'], content: string): boolean {
60
- if (role !== 'system' && looksPolluted(content)) {
61
- this.droppedCount++;
62
- // Keep a 1-line internal note so the user can see *something* happened,
63
- // but the original polluted text does NOT go to the LLM context.
64
- this.messages.push({
65
- role: 'user',
66
- content: '[system: dropped a polluted prior message containing "已中断" markers. Continue with the original task.]',
67
- name: 'system'
68
- });
69
- return false;
70
- }
71
-
72
- this.messages.push({
73
- role,
74
- content,
75
- name: role === 'user' ? 'user' : undefined
76
- });
77
- return true;
78
- }
79
-
80
- /**
81
- * Add a message WITHOUT any sanitization (for trusted callers, e.g. system prompt)
82
- */
83
- addMessageRaw(role: ChatMessage['role'], content: string): void {
84
- this.messages.push({ role, content });
85
- }
86
-
87
- /**
88
- * How many polluted messages have we filtered out? Useful for debugging
89
- * the "AI keeps saying interrupted" loop.
90
- */
91
- getDroppedCount(): number {
92
- return this.droppedCount;
93
- }
94
-
95
- /**
96
- * Return messages ready to send to the LLM. We additionally dedupe
97
- * adjacent identical assistant messages (a recovery hack against the
98
- * "AI repeats the same paragraph each turn" loop).
99
- */
100
- getMessages(): ChatMessage[] {
101
- const out: ChatMessage[] = [];
102
- for (let i = 0; i < this.messages.length; i++) {
103
- const m = this.messages[i];
104
- const prev = out[out.length - 1];
105
- if (
106
- prev &&
107
- m.role === 'assistant' &&
108
- prev.role === 'assistant' &&
109
- m.content.length > 0 &&
110
- m.content === prev.content
111
- ) {
112
- // collapse identical neighbour
113
- continue;
114
- }
115
- out.push(m);
116
- }
117
- return out;
118
- }
119
-
120
- /**
121
- * Get message count (raw, includes internal notes)
122
- */
123
- getMessageCount(): number {
124
- return this.messages.length;
125
- }
126
-
127
- /**
128
- * Clear session history
129
- */
130
- clear(): void {
131
- this.messages = [];
132
- this.droppedCount = 0;
133
- }
134
-
135
- /**
136
- * Get session info
137
- */
138
- getSession(): Session {
139
- return {
140
- id: this.sessionId,
141
- messages: this.messages,
142
- createdAt: this.createdAt,
143
- lastActiveAt: new Date()
144
- };
145
- }
146
-
147
- /**
148
- * Truncate messages to fit token limit
149
- */
150
- truncate(maxMessages: number = 20): void {
151
- if (this.messages.length > maxMessages) {
152
- const systemMsg = this.messages.find(m => m.role === 'system');
153
- const otherMsgs = this.messages.filter(m => m.role !== 'system');
154
-
155
- this.messages = [
156
- ...(systemMsg ? [systemMsg] : []),
157
- ...otherMsgs.slice(-(maxMessages - 1))
158
- ];
159
- }
160
- }
161
-
162
- private generateId(): string {
163
- return `session_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
164
- }
165
- }