kyawthiha-nextjs-agent-cli 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +188 -0
- package/dist/agent/agent.js +340 -0
- package/dist/agent/index.js +6 -0
- package/dist/agent/prompts/agent-prompt.js +527 -0
- package/dist/agent/summarizer.js +97 -0
- package/dist/agent/tools/ast-tools.js +601 -0
- package/dist/agent/tools/code-tools.js +1059 -0
- package/dist/agent/tools/file-tools.js +199 -0
- package/dist/agent/tools/index.js +25 -0
- package/dist/agent/tools/search-tools.js +404 -0
- package/dist/agent/tools/shell-tools.js +334 -0
- package/dist/agent/types.js +4 -0
- package/dist/cli/commands/config.js +61 -0
- package/dist/cli/commands/start.js +236 -0
- package/dist/cli/index.js +12 -0
- package/dist/utils/cred-store.js +70 -0
- package/dist/utils/logger.js +9 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# Next.js Fullstack Agent CLI
|
|
2
|
+
|
|
3
|
+
Gemini 3 Hackathon Entry 2026
|
|
4
|
+
|
|
5
|
+
**Gemini Next.js Agent CLI** is an autonomous, tool-driven developer agent that transforms natural language into verified, production-ready Next.js fullstack applications.
|
|
6
|
+
|
|
7
|
+
The agent plans tasks, executes real commands, verifies results, attempts repairs, and asks for manual input only when automation is unsafe.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
This CLI is designed to behave like a real developer inside a real project environment.
|
|
14
|
+
|
|
15
|
+
Instead of only generating code, the agent:
|
|
16
|
+
- Creates and modifies files directly
|
|
17
|
+
- Runs installs and builds
|
|
18
|
+
- Verifies results through execution
|
|
19
|
+
- Attempts automated fixes when failures occur
|
|
20
|
+
- Pauses for human input when decisions require clarity
|
|
21
|
+
|
|
22
|
+
The focus is reliability, correctness, and real execution.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- Runtime: Node.js
|
|
29
|
+
- Language: TypeScript
|
|
30
|
+
- CLI Framework: Commander, Inquirer
|
|
31
|
+
- Agent: Gemini-powered agentic workflow
|
|
32
|
+
- Planning before execution
|
|
33
|
+
- Real filesystem operations
|
|
34
|
+
- Build and runtime verification
|
|
35
|
+
- Targeted self-repair for failures
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Requirements
|
|
40
|
+
|
|
41
|
+
Before installing, ensure you have:
|
|
42
|
+
|
|
43
|
+
- Node.js 18 or later
|
|
44
|
+
- npm or pnpm
|
|
45
|
+
- PostgreSQL (optional, only required if your project uses a database)
|
|
46
|
+
- A Gemini API key
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation (Step by Step)
|
|
51
|
+
|
|
52
|
+
1. Clone the repository
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/kywthiha/nextjs-agent-cli.git
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. Move into the project directory
|
|
58
|
+
```bash
|
|
59
|
+
cd nextjs-agent-cli
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
3. Install dependencies
|
|
63
|
+
```bash
|
|
64
|
+
npm install
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
4. Configure environment variables
|
|
68
|
+
```bash
|
|
69
|
+
cp .env.example .env
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Add your Gemini API key to the `.env` file:
|
|
73
|
+
```
|
|
74
|
+
GEMINI_API_KEY=your_api_key_here
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Running the CLI
|
|
80
|
+
|
|
81
|
+
Start the CLI in development mode:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npm run dev
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The agent will launch an interactive session in your terminal.
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Interactive Workflow
|
|
92
|
+
|
|
93
|
+
### 1. Project Creation
|
|
94
|
+
|
|
95
|
+
Choose where the Next.js project should be created.
|
|
96
|
+
```
|
|
97
|
+
? Where should the project be created? (./my-app)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
### 2. Model Selection
|
|
103
|
+
|
|
104
|
+
Select the Gemini model for the agent.
|
|
105
|
+
- gemini-3-flash-preview
|
|
106
|
+
- gemini-3-pro-preview
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### 3. Database Configuration (Optional)
|
|
111
|
+
|
|
112
|
+
If your project requires a database, configure PostgreSQL:
|
|
113
|
+
```
|
|
114
|
+
--- PostgreSQL Configuration ---
|
|
115
|
+
? Host: localhost
|
|
116
|
+
? Port: 5432
|
|
117
|
+
? Username: postgres
|
|
118
|
+
? Password: [HIDDEN]
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The agent will:
|
|
122
|
+
- Verify the connection
|
|
123
|
+
- Create the database if it does not exist
|
|
124
|
+
- Handle schema setup when required
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
### 4. Define Your Goal
|
|
129
|
+
|
|
130
|
+
Describe what you want to build using natural language.
|
|
131
|
+
```
|
|
132
|
+
? What do you want to build or modify?
|
|
133
|
+
> Build inventory management for a mobile shop with POS and user management
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The agent will then:
|
|
137
|
+
- Generate an implementation plan (plan.md)
|
|
138
|
+
- Generate a task checklist (task.md)
|
|
139
|
+
- Execute tasks step by step
|
|
140
|
+
- Verify builds and runtime behavior
|
|
141
|
+
- Attempt fixes if errors occur
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Testing and Verification
|
|
146
|
+
|
|
147
|
+
Verification is a core part of the agent’s workflow.
|
|
148
|
+
|
|
149
|
+
During execution, the agent may:
|
|
150
|
+
- Run dependency installs
|
|
151
|
+
- Run builds
|
|
152
|
+
- Detect runtime or build failures
|
|
153
|
+
- Attempt automated fixes
|
|
154
|
+
- Re-run verification steps
|
|
155
|
+
|
|
156
|
+
If a failure requires a business or architectural decision, the agent will pause and request manual input instead of guessing.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Manual Testing (Optional)
|
|
161
|
+
|
|
162
|
+
After the agent finishes, you can manually verify the generated project:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cd my-app
|
|
166
|
+
npm run dev
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Open the browser at:
|
|
170
|
+
```
|
|
171
|
+
http://localhost:3000
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Limitations
|
|
177
|
+
|
|
178
|
+
- Some runtime issues require human decisions
|
|
179
|
+
- Complex architecture changes may need guidance
|
|
180
|
+
- The agent prioritizes safety and correctness over aggressive automation
|
|
181
|
+
|
|
182
|
+
These constraints are intentional.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## License
|
|
187
|
+
|
|
188
|
+
MIT License
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core AI Agent - Agentic loop pattern with Gemini 3 SDK
|
|
3
|
+
*/
|
|
4
|
+
import { GoogleGenAI, ThinkingLevel } from '@google/genai';
|
|
5
|
+
import { logger } from '../utils/logger.js';
|
|
6
|
+
import { AGENT_SYSTEM_PROMPT, createTaskPrompt } from './prompts/agent-prompt.js';
|
|
7
|
+
import { getAllTools } from './tools/index.js';
|
|
8
|
+
import { ConversationSummarizer } from './summarizer.js';
|
|
9
|
+
const MAX_API_RETRIES = 5;
|
|
10
|
+
const INITIAL_RETRY_DELAY_MS = 2000;
|
|
11
|
+
// Token management constants
|
|
12
|
+
const MAX_CONTEXT_TOKENS = 1000000; // 1M input context (Gemini 3 Pro/Flash), 64k output
|
|
13
|
+
const COMPRESSION_THRESHOLD = 0.7; // Compress at 70% capacity
|
|
14
|
+
const KEEP_RECENT_MESSAGES = 10; // Always keep last N messages
|
|
15
|
+
function sleep(ms) {
|
|
16
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
+
}
|
|
18
|
+
export class Agent {
|
|
19
|
+
client;
|
|
20
|
+
config;
|
|
21
|
+
tools;
|
|
22
|
+
conversation = [];
|
|
23
|
+
activeProjectPath = null;
|
|
24
|
+
isInitialized = false;
|
|
25
|
+
originalTaskPrompt = '';
|
|
26
|
+
summarizer;
|
|
27
|
+
lastTokenCount = 0; // Actual token count from last API response
|
|
28
|
+
constructor(config) {
|
|
29
|
+
this.config = config;
|
|
30
|
+
this.client = new GoogleGenAI({ apiKey: config.geminiApiKey });
|
|
31
|
+
this.tools = getAllTools();
|
|
32
|
+
this.summarizer = new ConversationSummarizer(config.geminiApiKey, config.modelName);
|
|
33
|
+
}
|
|
34
|
+
getThinkingLevel(modelName) {
|
|
35
|
+
return modelName.includes('pro') ? ThinkingLevel.HIGH : ThinkingLevel.MEDIUM;
|
|
36
|
+
}
|
|
37
|
+
async init() {
|
|
38
|
+
if (this.isInitialized)
|
|
39
|
+
return;
|
|
40
|
+
this.isInitialized = true;
|
|
41
|
+
}
|
|
42
|
+
async start(task) {
|
|
43
|
+
if (!this.isInitialized)
|
|
44
|
+
await this.init();
|
|
45
|
+
const { prompt, projectPath, databaseUrl } = task;
|
|
46
|
+
this.activeProjectPath = projectPath;
|
|
47
|
+
logger.step('Starting Full Stack Agent');
|
|
48
|
+
logger.info(`Goal: ${prompt}`);
|
|
49
|
+
logger.info(`Output: ${projectPath}`);
|
|
50
|
+
if (databaseUrl)
|
|
51
|
+
logger.info(`DB URL provided`);
|
|
52
|
+
const setupPrompt = createTaskPrompt(prompt, projectPath, databaseUrl);
|
|
53
|
+
this.originalTaskPrompt = setupPrompt;
|
|
54
|
+
// Check for previous summary to continue from
|
|
55
|
+
const previousSummary = await this.summarizer.loadSummary(projectPath);
|
|
56
|
+
if (previousSummary) {
|
|
57
|
+
logger.info('Found previous progress summary, continuing...');
|
|
58
|
+
this.conversation = [{
|
|
59
|
+
role: 'user',
|
|
60
|
+
parts: [{ text: `${setupPrompt}\n\n## Previous Progress:\n${previousSummary}\n\nContinue from where you left off.` }]
|
|
61
|
+
}];
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
this.conversation = [{
|
|
65
|
+
role: 'user',
|
|
66
|
+
parts: [{ text: setupPrompt }]
|
|
67
|
+
}];
|
|
68
|
+
}
|
|
69
|
+
await this.executeTaskLoop('Planning-Phase');
|
|
70
|
+
logger.step('Agent session ended - Implementation Complete');
|
|
71
|
+
}
|
|
72
|
+
async chat(message) {
|
|
73
|
+
if (!this.isInitialized)
|
|
74
|
+
await this.init();
|
|
75
|
+
if (!this.activeProjectPath) {
|
|
76
|
+
throw new Error('Agent has not been started. Call start() first.');
|
|
77
|
+
}
|
|
78
|
+
logger.step('Continuing Agent Session');
|
|
79
|
+
logger.info(`Request: ${message}`);
|
|
80
|
+
this.conversation.push({
|
|
81
|
+
role: 'user',
|
|
82
|
+
parts: [{ text: message }]
|
|
83
|
+
});
|
|
84
|
+
await this.executeTaskLoop('Follow-up-Phase');
|
|
85
|
+
logger.step('Request Completed');
|
|
86
|
+
}
|
|
87
|
+
async executeTaskLoop(contextId) {
|
|
88
|
+
let iteration = 0;
|
|
89
|
+
let isComplete = false;
|
|
90
|
+
const maxIterations = this.config.maxIterations;
|
|
91
|
+
while (!isComplete && iteration < maxIterations) {
|
|
92
|
+
iteration++;
|
|
93
|
+
logger.step(`[${contextId}] Iteration ${iteration}/${maxIterations}`);
|
|
94
|
+
// Check and compress conversation if needed
|
|
95
|
+
await this.checkAndCompressConversation();
|
|
96
|
+
try {
|
|
97
|
+
const response = await this.callAIWithRetry();
|
|
98
|
+
// Track actual token usage from SDK response
|
|
99
|
+
this.updateTokenCount(response);
|
|
100
|
+
const { text, toolCalls, functionResponseParts } = this.parseResponse(response);
|
|
101
|
+
if (text) {
|
|
102
|
+
logger.dim('AI: ' + text.substring(0, 200) + (text.length > 200 ? '...' : ''));
|
|
103
|
+
// Check for completion signal
|
|
104
|
+
if (text.includes('TASK COMPLETE')) {
|
|
105
|
+
logger.success(`[${contextId}] Task Complete!`);
|
|
106
|
+
isComplete = true;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (toolCalls.length === 0) {
|
|
111
|
+
if (!text) {
|
|
112
|
+
logger.warn('No response from AI, retrying...');
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
// AI just responded with text, ask it to continue
|
|
116
|
+
this.conversation.push({
|
|
117
|
+
role: 'user',
|
|
118
|
+
parts: [{ text: 'Continue with the task. Use tools as needed.' }]
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const toolResults = await this.executeTools(toolCalls);
|
|
123
|
+
this.conversation.push({
|
|
124
|
+
role: 'model',
|
|
125
|
+
parts: functionResponseParts.length > 0 ? functionResponseParts : [{ text: text || '' }]
|
|
126
|
+
});
|
|
127
|
+
this.conversation.push({
|
|
128
|
+
role: 'user',
|
|
129
|
+
parts: toolResults.map(r => ({
|
|
130
|
+
functionResponse: {
|
|
131
|
+
name: r.name,
|
|
132
|
+
response: { result: r.result }
|
|
133
|
+
}
|
|
134
|
+
}))
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
logger.error(`[${contextId}] Iteration ${iteration} error: ${error.message}`);
|
|
139
|
+
// Check if token limit exceeded
|
|
140
|
+
if (this.isTokenLimitError(error)) {
|
|
141
|
+
logger.warn('Token limit exceeded, generating summary and restarting...');
|
|
142
|
+
await this.handleTokenLimitExceeded();
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
this.conversation.push({
|
|
146
|
+
role: 'user',
|
|
147
|
+
parts: [{ text: `Error: ${error.message}. Try a different approach.` }]
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!isComplete) {
|
|
152
|
+
logger.warn(`[${contextId}] Reached max iterations (${maxIterations}) without completion signal`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async callAIWithRetry() {
|
|
156
|
+
let lastError = null;
|
|
157
|
+
for (let attempt = 1; attempt <= MAX_API_RETRIES; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
return await this.callAI();
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
lastError = error;
|
|
163
|
+
const errorMessage = error.message || '';
|
|
164
|
+
const errorString = JSON.stringify(error);
|
|
165
|
+
// Check if it's a retryable error (503, 429, overloaded, etc.)
|
|
166
|
+
const isRetryable = errorMessage.includes('503') ||
|
|
167
|
+
errorMessage.includes('429') ||
|
|
168
|
+
errorMessage.includes('overloaded') ||
|
|
169
|
+
errorMessage.includes('UNAVAILABLE') ||
|
|
170
|
+
errorMessage.includes('rate limit') ||
|
|
171
|
+
errorMessage.includes('quota') ||
|
|
172
|
+
errorString.includes('503') ||
|
|
173
|
+
errorString.includes('overloaded');
|
|
174
|
+
if (isRetryable && attempt < MAX_API_RETRIES) {
|
|
175
|
+
// Exponential backoff: 2s, 4s, 8s, 16s, 32s
|
|
176
|
+
const delayMs = INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt - 1);
|
|
177
|
+
logger.warn(`API error (${attempt}/${MAX_API_RETRIES}), retrying in ${delayMs / 1000}s...`);
|
|
178
|
+
await sleep(delayMs);
|
|
179
|
+
}
|
|
180
|
+
else if (!isRetryable) {
|
|
181
|
+
// Non-retryable error, throw immediately
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
// All retries exhausted
|
|
187
|
+
throw lastError || new Error('API call failed after max retries');
|
|
188
|
+
}
|
|
189
|
+
async callAI() {
|
|
190
|
+
const functionDeclarations = this.tools.map(t => ({
|
|
191
|
+
name: t.name,
|
|
192
|
+
description: t.description,
|
|
193
|
+
parametersJsonSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: Object.fromEntries(Object.entries(t.parameters.properties || {}).map(([key, value]) => [
|
|
196
|
+
key,
|
|
197
|
+
{
|
|
198
|
+
type: value.type,
|
|
199
|
+
description: value.description,
|
|
200
|
+
...(value.enum && { enum: value.enum })
|
|
201
|
+
}
|
|
202
|
+
])),
|
|
203
|
+
required: t.parameters.required || []
|
|
204
|
+
}
|
|
205
|
+
}));
|
|
206
|
+
const tools = [{ functionDeclarations }];
|
|
207
|
+
const modelName = this.config.modelName || 'gemini-3-flash-preview';
|
|
208
|
+
const thinkingLevel = this.getThinkingLevel(modelName);
|
|
209
|
+
const response = await this.client.models.generateContent({
|
|
210
|
+
model: modelName,
|
|
211
|
+
contents: this.conversation,
|
|
212
|
+
config: {
|
|
213
|
+
systemInstruction: AGENT_SYSTEM_PROMPT,
|
|
214
|
+
tools,
|
|
215
|
+
thinkingConfig: {
|
|
216
|
+
thinkingLevel,
|
|
217
|
+
},
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return response;
|
|
221
|
+
}
|
|
222
|
+
parseResponse(response) {
|
|
223
|
+
const parts = response.candidates?.[0]?.content?.parts || [];
|
|
224
|
+
let text = '';
|
|
225
|
+
const toolCalls = [];
|
|
226
|
+
const functionResponseParts = [];
|
|
227
|
+
for (const part of parts) {
|
|
228
|
+
if (part.text) {
|
|
229
|
+
text += part.text;
|
|
230
|
+
}
|
|
231
|
+
if (part.functionCall) {
|
|
232
|
+
toolCalls.push({
|
|
233
|
+
id: `call_${Date.now()}_${toolCalls.length}`,
|
|
234
|
+
name: part.functionCall.name || '',
|
|
235
|
+
arguments: part.functionCall.args || {}
|
|
236
|
+
});
|
|
237
|
+
functionResponseParts.push(part);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return { text, toolCalls, functionResponseParts };
|
|
241
|
+
}
|
|
242
|
+
async executeTools(toolCalls) {
|
|
243
|
+
const preparedCalls = toolCalls.map(call => {
|
|
244
|
+
const tool = this.tools.find(t => t.name === call.name);
|
|
245
|
+
// Enforce project path
|
|
246
|
+
if (this.activeProjectPath) {
|
|
247
|
+
if (call.arguments.cwd !== undefined) {
|
|
248
|
+
call.arguments.cwd = this.activeProjectPath;
|
|
249
|
+
}
|
|
250
|
+
if (call.arguments.projectPath !== undefined) {
|
|
251
|
+
call.arguments.projectPath = this.activeProjectPath;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return { call, tool };
|
|
255
|
+
});
|
|
256
|
+
const promises = preparedCalls.map(async ({ call, tool }) => {
|
|
257
|
+
if (!tool) {
|
|
258
|
+
logger.warn(`Tool not found: ${call.name}`);
|
|
259
|
+
return { name: call.name, result: `Error: Tool "${call.name}" not found` };
|
|
260
|
+
}
|
|
261
|
+
if (this.config.verbose) {
|
|
262
|
+
logger.dim(`Tool: ${call.name}(${JSON.stringify(call.arguments)})`);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
logger.info(` → ${call.name}`);
|
|
266
|
+
}
|
|
267
|
+
try {
|
|
268
|
+
const result = await tool.execute(call.arguments);
|
|
269
|
+
if (this.config.verbose) {
|
|
270
|
+
const preview = result.length > 100 ? result.substring(0, 100) + '...' : result;
|
|
271
|
+
logger.dim(` Result: ${preview}`);
|
|
272
|
+
}
|
|
273
|
+
return { name: call.name, result };
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
logger.error(`Tool ${call.name} failed: ${error.message}`);
|
|
277
|
+
return { name: call.name, result: `Error: ${error.message}` };
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
return await Promise.all(promises);
|
|
281
|
+
}
|
|
282
|
+
// ==================== Token Management ====================
|
|
283
|
+
isTokenLimitError(error) {
|
|
284
|
+
const msg = error.message?.toLowerCase() || '';
|
|
285
|
+
return msg.includes('token') ||
|
|
286
|
+
msg.includes('context length') ||
|
|
287
|
+
msg.includes('too long') ||
|
|
288
|
+
msg.includes('maximum context');
|
|
289
|
+
}
|
|
290
|
+
updateTokenCount(response) {
|
|
291
|
+
const usage = response.usageMetadata;
|
|
292
|
+
if (usage?.promptTokenCount) {
|
|
293
|
+
this.lastTokenCount = usage.promptTokenCount;
|
|
294
|
+
if (this.config.verbose) {
|
|
295
|
+
logger.dim(`Input tokens: ${this.lastTokenCount.toLocaleString()} / ${MAX_CONTEXT_TOKENS.toLocaleString()}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
async checkAndCompressConversation() {
|
|
300
|
+
const threshold = MAX_CONTEXT_TOKENS * COMPRESSION_THRESHOLD;
|
|
301
|
+
if (this.lastTokenCount > threshold) {
|
|
302
|
+
logger.info(`Token usage ${this.lastTokenCount.toLocaleString()}/${MAX_CONTEXT_TOKENS.toLocaleString()} (${Math.round(this.lastTokenCount / MAX_CONTEXT_TOKENS * 100)}%), compressing...`);
|
|
303
|
+
this.compressConversation();
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
compressConversation() {
|
|
307
|
+
if (this.conversation.length <= KEEP_RECENT_MESSAGES + 1) {
|
|
308
|
+
return; // Nothing to compress
|
|
309
|
+
}
|
|
310
|
+
// Keep first message (original task) and last N messages
|
|
311
|
+
const firstMessage = this.conversation[0];
|
|
312
|
+
const recentMessages = this.conversation.slice(-KEEP_RECENT_MESSAGES);
|
|
313
|
+
this.conversation = [firstMessage, ...recentMessages];
|
|
314
|
+
logger.dim(`Compressed conversation to ${this.conversation.length} messages`);
|
|
315
|
+
}
|
|
316
|
+
async handleTokenLimitExceeded() {
|
|
317
|
+
if (!this.activeProjectPath) {
|
|
318
|
+
this.compressConversation();
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
// Generate summary using Gemini
|
|
323
|
+
logger.dim('Generating progress summary with Gemini...');
|
|
324
|
+
const summary = await this.summarizer.generateSummary(this.conversation);
|
|
325
|
+
// Save to file
|
|
326
|
+
await this.summarizer.saveSummary(this.activeProjectPath, summary);
|
|
327
|
+
// Restart with fresh conversation including summary
|
|
328
|
+
this.conversation = [{
|
|
329
|
+
role: 'user',
|
|
330
|
+
parts: [{ text: `${this.originalTaskPrompt}\n\n## Previous Progress:\n${summary}\n\nContinue from where you left off.` }]
|
|
331
|
+
}];
|
|
332
|
+
logger.success('Conversation reset with AI-generated progress summary');
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
logger.error(`Failed to handle token limit: ${error.message}`);
|
|
336
|
+
// Fallback: just compress aggressively
|
|
337
|
+
this.compressConversation();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|