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.
- package/CHANGELOG.md +131 -0
- package/DEVELOPMENT.md +286 -0
- package/README.md +131 -30
- package/dist/core/ai-engine.d.ts +17 -0
- package/dist/core/ai-engine.d.ts.map +1 -1
- package/dist/core/ai-engine.js +23 -7
- package/dist/core/ai-engine.js.map +1 -1
- package/dist/index.js +18 -27
- package/dist/index.js.map +1 -1
- package/dist/repl/input.d.ts +8 -0
- package/dist/repl/input.d.ts.map +1 -1
- package/dist/repl/input.js +12 -0
- package/dist/repl/input.js.map +1 -1
- package/dist/repl/loop.d.ts +56 -0
- package/dist/repl/loop.d.ts.map +1 -1
- package/dist/repl/loop.js +333 -4
- package/dist/repl/loop.js.map +1 -1
- package/dist/repl/output.d.ts.map +1 -1
- package/dist/repl/output.js +3 -1
- package/dist/repl/output.js.map +1 -1
- package/dist/repl/welcome.js +1 -1
- package/dist/repl/welcome.js.map +1 -1
- package/docs/API_KEY_GUIDE.md +6 -0
- package/docs/FAQ.md +25 -3
- package/package.json +35 -3
- package/.patches/0001-fix-repl-replace-readline-with-inquirer-input-for-ke.patch +0 -279
- package/.patches/0002-fix-repl-stream-AI-output-directly-to-stdout-so-term.patch +0 -564
- package/.patches/0003-fix-session-break-the-hallucination-loop-in-assistan.patch +0 -194
- package/.patches/0004-chore-release-bump-version-to-0.2.1.patch +0 -24
- package/ROADMAP.md +0 -107
- package/src/agent/core.ts +0 -179
- package/src/agent/index.ts +0 -8
- package/src/agent/intent.ts +0 -181
- package/src/agent/streaming.ts +0 -132
- package/src/core/ai-engine.ts +0 -437
- package/src/core/cli.ts +0 -171
- package/src/core/config.ts +0 -147
- package/src/core/context-compactor.ts +0 -245
- package/src/core/hooks.ts +0 -196
- package/src/core/permissions.ts +0 -308
- package/src/core/session.ts +0 -165
- package/src/core/skills.ts +0 -208
- package/src/core/state.ts +0 -120
- package/src/core/subagent.ts +0 -195
- package/src/core/system-prompt.ts +0 -163
- package/src/core/tool-registry.ts +0 -157
- package/src/core/types.ts +0 -280
- package/src/index.ts +0 -544
- package/src/mcp/client.ts +0 -330
- package/src/repl/index.ts +0 -8
- package/src/repl/input.ts +0 -139
- package/src/repl/loop.ts +0 -280
- package/src/repl/output.ts +0 -222
- package/src/repl/welcome.ts +0 -296
- package/src/tools/file.ts +0 -117
- package/src/tools/git.ts +0 -132
- package/src/tools/index.ts +0 -48
- package/src/tools/search.ts +0 -263
- package/src/tools/shell.ts +0 -181
- package/src/utils/diff-preview.ts +0 -202
- package/src/utils/index.ts +0 -8
- package/src/utils/memory.ts +0 -223
- package/src/utils/project-context.ts +0 -207
- package/tsconfig.json +0 -19
package/src/core/cli.ts
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S12: CLI & Architecture
|
|
3
|
-
*
|
|
4
|
-
* Main entry point, CLI argument parsing, and module architecture overview.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { readFileSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { AIEngine } from './ai-engine.js';
|
|
10
|
-
import { ConfigManager } from './config.js';
|
|
11
|
-
import { REPLLoop } from '../repl/loop.js';
|
|
12
|
-
import { ToolRegistry } from './tool-registry.js';
|
|
13
|
-
import { ShellTool, FileTool, GitTool } from '../tools/index.js';
|
|
14
|
-
import { HookManager, auditLogHook, getHookManager } from './hooks.js';
|
|
15
|
-
import { getStateManager } from './state.js';
|
|
16
|
-
import { SystemPromptBuilder } from './system-prompt.js';
|
|
17
|
-
import { BUILT_IN_SKILLS } from './skills.js';
|
|
18
|
-
|
|
19
|
-
// ==================== CLI Argument Parsing ====================
|
|
20
|
-
|
|
21
|
-
export interface CLIOptions {
|
|
22
|
-
model?: string;
|
|
23
|
-
provider?: string;
|
|
24
|
-
prompt?: string;
|
|
25
|
-
noStream?: boolean;
|
|
26
|
-
verbose?: boolean;
|
|
27
|
-
hooks?: boolean;
|
|
28
|
-
permissionMode?: 'accept' | 'deny' | 'ask';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function parseArgs(args: string[]): CLIOptions {
|
|
32
|
-
const options: CLIOptions = {};
|
|
33
|
-
|
|
34
|
-
for (let i = 0; i < args.length; i++) {
|
|
35
|
-
const arg = args[i];
|
|
36
|
-
|
|
37
|
-
switch (arg) {
|
|
38
|
-
case '--model':
|
|
39
|
-
case '-m':
|
|
40
|
-
options.model = args[++i];
|
|
41
|
-
break;
|
|
42
|
-
case '--provider':
|
|
43
|
-
case '-p':
|
|
44
|
-
options.provider = args[++i];
|
|
45
|
-
break;
|
|
46
|
-
case '--prompt':
|
|
47
|
-
case '-c':
|
|
48
|
-
options.prompt = args[++i];
|
|
49
|
-
break;
|
|
50
|
-
case '--no-stream':
|
|
51
|
-
options.noStream = true;
|
|
52
|
-
break;
|
|
53
|
-
case '--verbose':
|
|
54
|
-
case '-v':
|
|
55
|
-
options.verbose = true;
|
|
56
|
-
break;
|
|
57
|
-
case '--hooks':
|
|
58
|
-
options.hooks = true;
|
|
59
|
-
break;
|
|
60
|
-
case '--permission':
|
|
61
|
-
options.permissionMode = (args[++i] as 'accept' | 'deny' | 'ask') || 'ask';
|
|
62
|
-
break;
|
|
63
|
-
case '--help':
|
|
64
|
-
case '-h':
|
|
65
|
-
printHelp();
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return options;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function printHelp(): void {
|
|
74
|
-
console.log(`
|
|
75
|
-
Thatgfsj Code - AI Coding Assistant
|
|
76
|
-
|
|
77
|
-
Usage: thatgfsj [options]
|
|
78
|
-
|
|
79
|
-
Options:
|
|
80
|
-
-m, --model <model> Set AI model
|
|
81
|
-
-p, --provider <provider> Set provider (siliconflow, minimax, openai, etc.)
|
|
82
|
-
-c, --prompt <text> Run single prompt and exit
|
|
83
|
-
--no-stream Disable streaming output
|
|
84
|
-
--hooks Enable built-in hooks (audit log, etc.)
|
|
85
|
-
--permission <mode> Set permission mode (accept, deny, ask)
|
|
86
|
-
-v, --verbose Verbose output
|
|
87
|
-
-h, --help Show this help
|
|
88
|
-
|
|
89
|
-
Examples:
|
|
90
|
-
thatgfsj Start REPL
|
|
91
|
-
thatgfsj -c "Hello" Run single prompt
|
|
92
|
-
thatgfsj --provider minimax --model MiniMax-M2.5 Use specific provider
|
|
93
|
-
thatgfsj --permission accept Accept all tool calls
|
|
94
|
-
`);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ==================== Architecture ====================
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* S12: Print architecture overview
|
|
101
|
-
*/
|
|
102
|
-
export function printArchitecture(): void {
|
|
103
|
-
console.log(`
|
|
104
|
-
Thatgfsj Code Architecture
|
|
105
|
-
============================
|
|
106
|
-
|
|
107
|
-
Core:
|
|
108
|
-
AIEngine - Async generator agent loop (S01), multi-provider
|
|
109
|
-
ToolRegistry - Tool registration and execution (S02)
|
|
110
|
-
PermissionChecker - 6-stage permission pipeline (S03)
|
|
111
|
-
SystemPromptBuilder - Dynamic prompt construction (S04)
|
|
112
|
-
ContextCompactor - 3-tier context compression (S05)
|
|
113
|
-
SubagentManager - Parallel subagent execution (S06)
|
|
114
|
-
HookManager - Event-driven hooks (S08)
|
|
115
|
-
SkillsLoader - Skills discovery and catalog (S10)
|
|
116
|
-
StateManager - Ephemeral/Persistent/Session state (S11)
|
|
117
|
-
|
|
118
|
-
Tools:
|
|
119
|
-
ShellTool - Shell command execution
|
|
120
|
-
FileTool - File read/write/list/delete
|
|
121
|
-
GitTool - Git operations
|
|
122
|
-
|
|
123
|
-
Entry:
|
|
124
|
-
REPLLoop - Interactive REPL
|
|
125
|
-
CLI - Command-line mode
|
|
126
|
-
`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ==================== Main ====================
|
|
130
|
-
|
|
131
|
-
export async function main(argv: string[]): Promise<void> {
|
|
132
|
-
const options = parseArgs(argv.slice(2));
|
|
133
|
-
|
|
134
|
-
// Single prompt mode
|
|
135
|
-
if (options.prompt) {
|
|
136
|
-
const config = await ConfigManager.load();
|
|
137
|
-
if (options.provider) config.provider = options.provider as any;
|
|
138
|
-
if (options.model) config.model = options.model;
|
|
139
|
-
|
|
140
|
-
const ai = new AIEngine(config);
|
|
141
|
-
const tools = [new ShellTool(), new FileTool(), new GitTool()];
|
|
142
|
-
for (const tool of tools) ai.registerTool(tool);
|
|
143
|
-
|
|
144
|
-
if (options.hooks) {
|
|
145
|
-
const hooks = getHookManager();
|
|
146
|
-
hooks.register('afterToolCall', auditLogHook);
|
|
147
|
-
ai.setHooks(hooks);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const promptBuilder = new SystemPromptBuilder({ tools, permissionMode: options.permissionMode });
|
|
151
|
-
const messages = [
|
|
152
|
-
{ role: 'system' as const, content: promptBuilder.build() },
|
|
153
|
-
{ role: 'user' as const, content: options.prompt }
|
|
154
|
-
];
|
|
155
|
-
|
|
156
|
-
if (options.noStream) {
|
|
157
|
-
const response = await ai.chat(messages);
|
|
158
|
-
console.log(response.content);
|
|
159
|
-
} else {
|
|
160
|
-
for await (const chunk of ai.chatStream(messages)) {
|
|
161
|
-
process.stdout.write(chunk);
|
|
162
|
-
}
|
|
163
|
-
console.log();
|
|
164
|
-
}
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// REPL mode
|
|
169
|
-
const repl = new REPLLoop();
|
|
170
|
-
await repl.start();
|
|
171
|
-
}
|
package/src/core/config.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Config Manager - Handles configuration loading and saving
|
|
3
|
-
* Supports multiple AI providers: MiniMax, SiliconFlow, OpenAI, Anthropic
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
|
7
|
-
import { join, dirname } from 'path';
|
|
8
|
-
import { homedir } from 'os';
|
|
9
|
-
import { Config, PROVIDERS, AIConfig } from './types.js';
|
|
10
|
-
|
|
11
|
-
const DEFAULT_CONFIG: Config = {
|
|
12
|
-
model: 'Qwen/Qwen2.5-7B-Instruct',
|
|
13
|
-
apiKey: '',
|
|
14
|
-
temperature: 0.7,
|
|
15
|
-
maxTokens: 4096,
|
|
16
|
-
provider: 'siliconflow',
|
|
17
|
-
baseUrl: 'https://api.siliconflow.cn/v1'
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export class ConfigManager {
|
|
21
|
-
private configPath: string;
|
|
22
|
-
|
|
23
|
-
constructor() {
|
|
24
|
-
const homeDir = homedir();
|
|
25
|
-
const configDir = join(homeDir, '.thatgfsj');
|
|
26
|
-
this.configPath = join(configDir, 'config.json');
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Load configuration with environment variable support
|
|
31
|
-
*/
|
|
32
|
-
static async load(): Promise<Config> {
|
|
33
|
-
const manager = new ConfigManager();
|
|
34
|
-
return manager.loadConfig();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Load config from file and merge with environment variables
|
|
39
|
-
*/
|
|
40
|
-
private loadConfig(): Config {
|
|
41
|
-
let config = { ...DEFAULT_CONFIG };
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
if (existsSync(this.configPath)) {
|
|
45
|
-
const data = readFileSync(this.configPath, 'utf-8');
|
|
46
|
-
const loaded = JSON.parse(data);
|
|
47
|
-
config = { ...config, ...loaded };
|
|
48
|
-
}
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.warn('Failed to load config, using defaults:', error);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Resolve provider and get API key from environment
|
|
54
|
-
config = this.resolveProvider(config);
|
|
55
|
-
|
|
56
|
-
return config;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Resolve provider and API key from config or environment
|
|
61
|
-
*/
|
|
62
|
-
private resolveProvider(config: Config): Config {
|
|
63
|
-
const provider = config.provider || 'siliconflow';
|
|
64
|
-
const providerConfig = (provider !== 'custom' && provider !== undefined) ? PROVIDERS[provider] : null;
|
|
65
|
-
|
|
66
|
-
if (!providerConfig) {
|
|
67
|
-
console.warn(`Unknown provider: ${provider}, falling back to siliconflow`);
|
|
68
|
-
return { ...config, provider: 'siliconflow', baseUrl: PROVIDERS.siliconflow.baseUrl };
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// MODEL environment variable has highest priority
|
|
72
|
-
let model = process.env.MODEL || config.model || providerConfig.defaultModel;
|
|
73
|
-
|
|
74
|
-
// Handle Ollama (local, no API key needed)
|
|
75
|
-
let apiKey = config.apiKey;
|
|
76
|
-
let baseUrl = config.baseUrl || providerConfig.baseUrl;
|
|
77
|
-
|
|
78
|
-
if (provider === 'ollama') {
|
|
79
|
-
// Ollama doesn't need an API key
|
|
80
|
-
apiKey = '';
|
|
81
|
-
// Allow custom base URL via environment
|
|
82
|
-
baseUrl = process.env.OLLAMA_BASE_URL || baseUrl;
|
|
83
|
-
} else {
|
|
84
|
-
// Try to get API key from environment
|
|
85
|
-
for (const envKey of providerConfig.envKeys) {
|
|
86
|
-
if (process.env[envKey]) {
|
|
87
|
-
apiKey = process.env[envKey];
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
...config,
|
|
95
|
-
provider,
|
|
96
|
-
baseUrl,
|
|
97
|
-
model,
|
|
98
|
-
apiKey: apiKey || ''
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Get AIConfig for AIEngine
|
|
104
|
-
*/
|
|
105
|
-
static async getAIConfig(): Promise<AIConfig> {
|
|
106
|
-
const config = await ConfigManager.load();
|
|
107
|
-
return {
|
|
108
|
-
model: config.model,
|
|
109
|
-
apiKey: config.apiKey,
|
|
110
|
-
temperature: config.temperature,
|
|
111
|
-
maxTokens: config.maxTokens,
|
|
112
|
-
baseUrl: config.baseUrl,
|
|
113
|
-
provider: config.provider
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Save configuration
|
|
119
|
-
*/
|
|
120
|
-
async save(config: Partial<Config>): Promise<void> {
|
|
121
|
-
const current = this.loadConfig();
|
|
122
|
-
const merged = { ...current, ...config };
|
|
123
|
-
|
|
124
|
-
const dir = dirname(this.configPath);
|
|
125
|
-
if (!existsSync(dir)) {
|
|
126
|
-
mkdirSync(dir, { recursive: true });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
writeFileSync(this.configPath, JSON.stringify(merged, null, 2));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Get config path
|
|
134
|
-
*/
|
|
135
|
-
getPath(): string {
|
|
136
|
-
return this.configPath;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* List available providers
|
|
141
|
-
*/
|
|
142
|
-
static listProviders(): string {
|
|
143
|
-
return Object.entries(PROVIDERS)
|
|
144
|
-
.map(([key, val]) => ` ${key}: ${val.name}`)
|
|
145
|
-
.join('\n');
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* S05: Context Compactor - 3-tier progressive compression strategy
|
|
3
|
-
*
|
|
4
|
-
* Tier 1: Truncation - Remove oldest messages beyond hard limit
|
|
5
|
-
* Tier 2: Summarize - Replace old message ranges with summary
|
|
6
|
-
* Tier 3: Selective - Keep only recent N turns around tool executions
|
|
7
|
-
*
|
|
8
|
-
* Core insight: Context window is finite. Three-tier strategy enables "infinite" work.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { ChatMessage } from './types.js';
|
|
12
|
-
|
|
13
|
-
export interface CompactorConfig {
|
|
14
|
-
/** Hard limit: messages beyond this get removed first */
|
|
15
|
-
hardLimit?: number;
|
|
16
|
-
/** Soft limit: messages beyond this get summarized */
|
|
17
|
-
softLimit?: number;
|
|
18
|
-
/** How many recent turns to always preserve */
|
|
19
|
-
preserveRecent?: number;
|
|
20
|
-
/** Enable summarization (requires AI API) */
|
|
21
|
-
enableSummarize?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface CompressionResult {
|
|
25
|
-
originalCount: number;
|
|
26
|
-
compactedCount: number;
|
|
27
|
-
removedCount: number;
|
|
28
|
-
method: 'none' | 'truncation' | 'summarize' | 'selective';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* S05: Context Compactor
|
|
33
|
-
*/
|
|
34
|
-
export class ContextCompactor {
|
|
35
|
-
private config: CompactorConfig;
|
|
36
|
-
private totalTokensUsed = 0;
|
|
37
|
-
|
|
38
|
-
constructor(config: CompactorConfig = {}) {
|
|
39
|
-
this.config = {
|
|
40
|
-
hardLimit: 50,
|
|
41
|
-
softLimit: 30,
|
|
42
|
-
preserveRecent: 10,
|
|
43
|
-
enableSummarize: false,
|
|
44
|
-
...config
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* S05: Compact messages using 3-tier strategy
|
|
50
|
-
* Called at the end of each agent turn
|
|
51
|
-
*/
|
|
52
|
-
compact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
|
|
53
|
-
const count = messages.length;
|
|
54
|
-
|
|
55
|
-
// Tier 1: Within hard limit — no compression
|
|
56
|
-
if (count <= (this.config.hardLimit || 50)) {
|
|
57
|
-
return {
|
|
58
|
-
compacted: messages,
|
|
59
|
-
result: { originalCount: count, compactedCount: count, removedCount: 0, method: 'none' }
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Tier 2: Beyond soft limit — summarize middle messages
|
|
64
|
-
if (count > (this.config.softLimit || 30)) {
|
|
65
|
-
return this.summarizeStrategy(messages);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Tier 3: Between soft and hard — selective preservation
|
|
69
|
-
return this.selectiveStrategy(messages);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Tier 1: Simple truncation
|
|
73
|
-
private truncate(messages: ChatMessage[], maxMessages: number): ChatMessage[] {
|
|
74
|
-
const systemMsg = messages.find(m => m.role === 'system');
|
|
75
|
-
const others = messages.filter(m => m.role !== 'system');
|
|
76
|
-
const toRemove = others.length - maxMessages;
|
|
77
|
-
|
|
78
|
-
if (toRemove <= 0 || others.length === 0) {
|
|
79
|
-
return messages;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Remove oldest non-system messages
|
|
83
|
-
const keptOthers = others.slice(toRemove);
|
|
84
|
-
return systemMsg ? [systemMsg, ...keptOthers] : keptOthers;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Tier 2: Summarize middle messages (stub — needs AI for real summarization)
|
|
88
|
-
private summarizeStrategy(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
|
|
89
|
-
const preserved = this.config.preserveRecent || 10;
|
|
90
|
-
const systemMsg = messages.find(m => m.role === 'system');
|
|
91
|
-
const others = messages.filter(m => m.role !== 'system');
|
|
92
|
-
|
|
93
|
-
if (others.length <= preserved) {
|
|
94
|
-
return {
|
|
95
|
-
compacted: messages,
|
|
96
|
-
result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0, method: 'none' }
|
|
97
|
-
};
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Keep recent N turns
|
|
101
|
-
const recent = others.slice(-preserved);
|
|
102
|
-
|
|
103
|
-
// Summarize the middle (this is a placeholder — real impl would call AI)
|
|
104
|
-
const summaryMsg: ChatMessage = {
|
|
105
|
-
role: 'system',
|
|
106
|
-
content: '[S05: ' + (others.length - preserved) + ' earlier messages summarized — ' +
|
|
107
|
-
'conversation had ' + (others.length - preserved) + ' exchanges covering ' +
|
|
108
|
-
this.summarizeTopic(others.slice(0, -preserved)) + ']'
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const compacted = systemMsg
|
|
112
|
-
? [systemMsg, summaryMsg, ...recent]
|
|
113
|
-
: [summaryMsg, ...recent];
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
compacted,
|
|
117
|
-
result: {
|
|
118
|
-
originalCount: messages.length,
|
|
119
|
-
compactedCount: compacted.length,
|
|
120
|
-
removedCount: messages.length - compacted.length,
|
|
121
|
-
method: 'summarize'
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Extract simple topic from message content
|
|
127
|
-
private summarizeTopic(msgs: ChatMessage[]): string {
|
|
128
|
-
const texts = msgs
|
|
129
|
-
.filter(m => m.role === 'user')
|
|
130
|
-
.map(m => m.content)
|
|
131
|
-
.slice(0, 3)
|
|
132
|
-
.join(' ')
|
|
133
|
-
.substring(0, 50);
|
|
134
|
-
return texts || 'various topics';
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Tier 3: Selective — keep turns around tool executions
|
|
138
|
-
private selectiveStrategy(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
|
|
139
|
-
const systemMsg = messages.find(m => m.role === 'system');
|
|
140
|
-
const others = messages.filter(m => m.role !== 'system');
|
|
141
|
-
const max = this.config.softLimit || 30;
|
|
142
|
-
|
|
143
|
-
if (others.length <= max) {
|
|
144
|
-
return {
|
|
145
|
-
compacted: messages,
|
|
146
|
-
result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0, method: 'none' }
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Find indices of tool-related messages
|
|
151
|
-
const toolIndices = new Set<number>();
|
|
152
|
-
others.forEach((m, i) => {
|
|
153
|
-
if (m.tool_calls || m.role === 'tool' || m.role === 'assistant') {
|
|
154
|
-
toolIndices.add(i);
|
|
155
|
-
}
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// Also add indices around tool messages
|
|
159
|
-
for (const idx of toolIndices) {
|
|
160
|
-
for (let offset = -2; offset <= 2; offset++) {
|
|
161
|
-
toolIndices.add(idx + offset);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Keep system message + messages near tool executions + recent messages
|
|
166
|
-
const recentCount = Math.floor(max * 0.3); // 30% recent
|
|
167
|
-
const toolCount = max - recentCount;
|
|
168
|
-
|
|
169
|
-
const recent = others.slice(-recentCount);
|
|
170
|
-
const toolMsgs: ChatMessage[] = [];
|
|
171
|
-
for (const idx of [...toolIndices].sort((a, b) => a - b)) {
|
|
172
|
-
if (toolMsgs.length >= toolCount) break;
|
|
173
|
-
if (idx >= 0 && idx < others.length - recentCount) {
|
|
174
|
-
toolMsgs.push(others[idx]);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const compacted = systemMsg
|
|
179
|
-
? [systemMsg, ...toolMsgs, ...recent]
|
|
180
|
-
: [...toolMsgs, ...recent];
|
|
181
|
-
|
|
182
|
-
// Deduplicate while preserving order
|
|
183
|
-
const seen = new Set<string>();
|
|
184
|
-
const deduped = compacted.filter(m => {
|
|
185
|
-
const key = m.role + m.content.substring(0, 30);
|
|
186
|
-
if (seen.has(key)) return false;
|
|
187
|
-
seen.add(key);
|
|
188
|
-
return true;
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
return {
|
|
192
|
-
compacted: deduped,
|
|
193
|
-
result: {
|
|
194
|
-
originalCount: messages.length,
|
|
195
|
-
compactedCount: deduped.length,
|
|
196
|
-
removedCount: messages.length - deduped.length,
|
|
197
|
-
method: 'selective'
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// S05: Check if context is approaching limit
|
|
203
|
-
isNearLimit(messages: ChatMessage[], limit: number = 150000): boolean {
|
|
204
|
-
// Rough estimate: 1 message ~= 200 tokens on average
|
|
205
|
-
const estimatedTokens = messages.length * 200;
|
|
206
|
-
return estimatedTokens > limit * 0.8; // 80% of limit
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// S05: Get estimated token count
|
|
210
|
-
estimateTokens(messages: ChatMessage[]): number {
|
|
211
|
-
return messages.reduce((sum, m) => {
|
|
212
|
-
// Rough: content chars / 4 tokens
|
|
213
|
-
return sum + Math.ceil(m.content.length / 4) + 10; // +10 for role/overhead
|
|
214
|
-
}, 0);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// S05: Auto-compact if needed (called from AIEngine or SessionManager)
|
|
218
|
-
autoCompact(messages: ChatMessage[]): { compacted: ChatMessage[]; result: CompressionResult } {
|
|
219
|
-
if (this.isNearLimit(messages)) {
|
|
220
|
-
return this.compact(messages);
|
|
221
|
-
}
|
|
222
|
-
return {
|
|
223
|
-
compacted: messages,
|
|
224
|
-
result: { originalCount: messages.length, compactedCount: messages.length, removedCount: 0, method: 'none' }
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ==================== SessionManager Integration ====================
|
|
230
|
-
|
|
231
|
-
export function integrateWithSessionManager(SessionManagerClass: any) {
|
|
232
|
-
const originalTruncate = SessionManagerClass.prototype.truncate;
|
|
233
|
-
|
|
234
|
-
SessionManagerClass.prototype.truncate = function(maxMessages: number, forceCompact = false) {
|
|
235
|
-
if (forceCompact) {
|
|
236
|
-
const compactor = new ContextCompactor({ hardLimit: maxMessages });
|
|
237
|
-
const { compacted, result } = compactor.autoCompact(this.messages);
|
|
238
|
-
if (result.method !== 'none') {
|
|
239
|
-
this.messages = compacted;
|
|
240
|
-
return result;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return originalTruncate.call(this, maxMessages);
|
|
244
|
-
};
|
|
245
|
-
}
|