gencode-ai 0.1.2 → 0.2.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 +15 -17
- package/dist/agent/agent.d.ts +43 -0
- package/dist/agent/agent.d.ts.map +1 -1
- package/dist/agent/agent.js +107 -4
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/types.d.ts +20 -1
- package/dist/agent/types.d.ts.map +1 -1
- package/dist/checkpointing/checkpoint-manager.d.ts +87 -0
- package/dist/checkpointing/checkpoint-manager.d.ts.map +1 -0
- package/dist/checkpointing/checkpoint-manager.js +281 -0
- package/dist/checkpointing/checkpoint-manager.js.map +1 -0
- package/dist/checkpointing/index.d.ts +29 -0
- package/dist/checkpointing/index.d.ts.map +1 -0
- package/dist/checkpointing/index.js +29 -0
- package/dist/checkpointing/index.js.map +1 -0
- package/dist/checkpointing/types.d.ts +98 -0
- package/dist/checkpointing/types.d.ts.map +1 -0
- package/dist/checkpointing/types.js +7 -0
- package/dist/checkpointing/types.js.map +1 -0
- package/dist/cli/components/App.d.ts.map +1 -1
- package/dist/cli/components/App.js +193 -7
- package/dist/cli/components/App.js.map +1 -1
- package/dist/cli/components/CommandSuggestions.d.ts.map +1 -1
- package/dist/cli/components/CommandSuggestions.js +5 -0
- package/dist/cli/components/CommandSuggestions.js.map +1 -1
- package/dist/cli/components/Messages.d.ts +7 -1
- package/dist/cli/components/Messages.d.ts.map +1 -1
- package/dist/cli/components/Messages.js +28 -2
- package/dist/cli/components/Messages.js.map +1 -1
- package/dist/cli/components/ModeIndicator.d.ts +42 -0
- package/dist/cli/components/ModeIndicator.d.ts.map +1 -0
- package/dist/cli/components/ModeIndicator.js +52 -0
- package/dist/cli/components/ModeIndicator.js.map +1 -0
- package/dist/cli/components/PlanApproval.d.ts +36 -0
- package/dist/cli/components/PlanApproval.d.ts.map +1 -0
- package/dist/cli/components/PlanApproval.js +154 -0
- package/dist/cli/components/PlanApproval.js.map +1 -0
- package/dist/cli/components/QuestionPrompt.d.ts +23 -0
- package/dist/cli/components/QuestionPrompt.d.ts.map +1 -0
- package/dist/cli/components/QuestionPrompt.js +231 -0
- package/dist/cli/components/QuestionPrompt.js.map +1 -0
- package/dist/cli/components/index.d.ts +1 -0
- package/dist/cli/components/index.d.ts.map +1 -1
- package/dist/cli/components/index.js +1 -0
- package/dist/cli/components/index.js.map +1 -1
- package/dist/cli/components/theme.d.ts +9 -0
- package/dist/cli/components/theme.d.ts.map +1 -1
- package/dist/cli/components/theme.js +14 -1
- package/dist/cli/components/theme.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/permissions/types.d.ts.map +1 -1
- package/dist/permissions/types.js +2 -0
- package/dist/permissions/types.js.map +1 -1
- package/dist/planning/index.d.ts +13 -0
- package/dist/planning/index.d.ts.map +1 -0
- package/dist/planning/index.js +15 -0
- package/dist/planning/index.js.map +1 -0
- package/dist/planning/plan-file.d.ts +59 -0
- package/dist/planning/plan-file.d.ts.map +1 -0
- package/dist/planning/plan-file.js +278 -0
- package/dist/planning/plan-file.js.map +1 -0
- package/dist/planning/state.d.ts +127 -0
- package/dist/planning/state.d.ts.map +1 -0
- package/dist/planning/state.js +261 -0
- package/dist/planning/state.js.map +1 -0
- package/dist/planning/tools/enter-plan-mode.d.ts +25 -0
- package/dist/planning/tools/enter-plan-mode.d.ts.map +1 -0
- package/dist/planning/tools/enter-plan-mode.js +98 -0
- package/dist/planning/tools/enter-plan-mode.js.map +1 -0
- package/dist/planning/tools/exit-plan-mode.d.ts +24 -0
- package/dist/planning/tools/exit-plan-mode.d.ts.map +1 -0
- package/dist/planning/tools/exit-plan-mode.js +149 -0
- package/dist/planning/tools/exit-plan-mode.js.map +1 -0
- package/dist/planning/types.d.ts +100 -0
- package/dist/planning/types.d.ts.map +1 -0
- package/dist/planning/types.js +28 -0
- package/dist/planning/types.js.map +1 -0
- package/dist/pricing/calculator.d.ts +21 -0
- package/dist/pricing/calculator.d.ts.map +1 -0
- package/dist/pricing/calculator.js +59 -0
- package/dist/pricing/calculator.js.map +1 -0
- package/dist/pricing/index.d.ts +7 -0
- package/dist/pricing/index.d.ts.map +1 -0
- package/dist/pricing/index.js +7 -0
- package/dist/pricing/index.js.map +1 -0
- package/dist/pricing/models.d.ts +20 -0
- package/dist/pricing/models.d.ts.map +1 -0
- package/dist/pricing/models.js +322 -0
- package/dist/pricing/models.js.map +1 -0
- package/dist/pricing/types.d.ts +30 -0
- package/dist/pricing/types.d.ts.map +1 -0
- package/dist/pricing/types.js +5 -0
- package/dist/pricing/types.js.map +1 -0
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +17 -10
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +21 -14
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +12 -8
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/types.d.ts +2 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/providers/vertex-ai.d.ts.map +1 -1
- package/dist/providers/vertex-ai.js +17 -10
- package/dist/providers/vertex-ai.js.map +1 -1
- package/dist/session/manager.d.ts +4 -0
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +8 -0
- package/dist/session/manager.js.map +1 -1
- package/dist/tools/builtin/ask-user.d.ts +64 -0
- package/dist/tools/builtin/ask-user.d.ts.map +1 -0
- package/dist/tools/builtin/ask-user.js +148 -0
- package/dist/tools/builtin/ask-user.js.map +1 -0
- package/dist/tools/index.d.ts +19 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +11 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/registry.d.ts +13 -0
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +79 -2
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/types.d.ts +17 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/dist/tools/types.js.map +1 -1
- package/docs/cost-tracking-comparison.md +904 -0
- package/docs/operating-modes.md +96 -0
- package/docs/proposals/0012-ask-user-question.md +66 -1
- package/docs/proposals/0025-cost-tracking.md +60 -2
- package/docs/proposals/README.md +2 -2
- package/examples/test-ask-user.ts +167 -0
- package/examples/test-checkpointing.ts +121 -0
- package/examples/test-cost-tracking.ts +77 -0
- package/examples/test-interrupt-cleanup.ts +94 -0
- package/package.json +1 -1
- package/src/agent/agent.ts +130 -4
- package/src/agent/index.ts +1 -0
- package/src/agent/types.ts +19 -1
- package/src/checkpointing/checkpoint-manager.ts +327 -0
- package/src/checkpointing/index.ts +45 -0
- package/src/checkpointing/types.ts +104 -0
- package/src/cli/components/App.tsx +259 -8
- package/src/cli/components/CommandSuggestions.tsx +5 -0
- package/src/cli/components/Messages.tsx +66 -4
- package/src/cli/components/ModeIndicator.tsx +174 -0
- package/src/cli/components/PlanApproval.tsx +327 -0
- package/src/cli/components/QuestionPrompt.tsx +462 -0
- package/src/cli/components/index.ts +1 -0
- package/src/cli/components/theme.ts +14 -1
- package/src/index.ts +15 -0
- package/src/permissions/types.ts +2 -0
- package/src/planning/index.ts +53 -0
- package/src/planning/plan-file.ts +326 -0
- package/src/planning/state.ts +305 -0
- package/src/planning/tools/enter-plan-mode.ts +111 -0
- package/src/planning/tools/exit-plan-mode.ts +170 -0
- package/src/planning/types.ts +150 -0
- package/src/pricing/calculator.ts +71 -0
- package/src/pricing/index.ts +7 -0
- package/src/pricing/models.ts +334 -0
- package/src/pricing/types.ts +32 -0
- package/src/prompts/system/base.txt +42 -0
- package/src/prompts/tools/ask-user.txt +110 -0
- package/src/providers/anthropic.ts +21 -10
- package/src/providers/gemini.ts +25 -14
- package/src/providers/openai.ts +17 -8
- package/src/providers/types.ts +3 -0
- package/src/providers/vertex-ai.ts +21 -10
- package/src/session/manager.ts +9 -0
- package/src/tools/builtin/ask-user.ts +185 -0
- package/src/tools/index.ts +23 -0
- package/src/tools/registry.ts +95 -2
- package/src/tools/types.ts +18 -0
- package/.gencode/settings.local.json +0 -7
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test Interrupt Cleanup
|
|
3
|
+
*
|
|
4
|
+
* Simulates the interrupt scenario and verifies cleanup works correctly
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Agent } from '../src/agent/index.js';
|
|
8
|
+
import type { Message } from '../src/providers/types.js';
|
|
9
|
+
|
|
10
|
+
async function testInterruptCleanup() {
|
|
11
|
+
console.log('🧪 Testing Interrupt Cleanup\n');
|
|
12
|
+
|
|
13
|
+
const agent = new Agent({
|
|
14
|
+
provider: 'anthropic',
|
|
15
|
+
model: 'claude-3-5-sonnet-20241022',
|
|
16
|
+
cwd: process.cwd(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
// Simulate incomplete tool_use message (what happens during interrupt)
|
|
21
|
+
console.log('1️⃣ Simulating incomplete tool_use message...');
|
|
22
|
+
|
|
23
|
+
// Access private messages array via getHistory and direct manipulation
|
|
24
|
+
const history = agent.getHistory();
|
|
25
|
+
console.log(` Initial history: ${history.length} messages`);
|
|
26
|
+
|
|
27
|
+
// Add a user message
|
|
28
|
+
(agent as any).messages.push({
|
|
29
|
+
role: 'user',
|
|
30
|
+
content: 'Create a file',
|
|
31
|
+
});
|
|
32
|
+
console.log(` After user message: ${agent.getHistory().length} messages`);
|
|
33
|
+
|
|
34
|
+
// Add an assistant message with tool_use (simulating interrupt point)
|
|
35
|
+
(agent as any).messages.push({
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
content: [
|
|
38
|
+
{ type: 'text', text: 'I will create the file.' },
|
|
39
|
+
{
|
|
40
|
+
type: 'tool_use',
|
|
41
|
+
id: 'toolu_test_123',
|
|
42
|
+
name: 'Write',
|
|
43
|
+
input: { file_path: 'test.txt', content: 'hello' },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
console.log(` After assistant+tool_use: ${agent.getHistory().length} messages`);
|
|
48
|
+
console.log(` Last message role: ${agent.getHistory()[agent.getHistory().length - 1].role}`);
|
|
49
|
+
|
|
50
|
+
// Verify incomplete state
|
|
51
|
+
const lastMessage = agent.getHistory()[agent.getHistory().length - 1];
|
|
52
|
+
const hasToolUse = Array.isArray(lastMessage.content) &&
|
|
53
|
+
lastMessage.content.some((c: any) => c.type === 'tool_use');
|
|
54
|
+
console.log(` Has incomplete tool_use: ${hasToolUse}\n`);
|
|
55
|
+
|
|
56
|
+
console.log('2️⃣ Calling cleanupIncompleteMessages()...');
|
|
57
|
+
agent.cleanupIncompleteMessages();
|
|
58
|
+
|
|
59
|
+
const afterCleanup = agent.getHistory();
|
|
60
|
+
console.log(` After cleanup: ${afterCleanup.length} messages`);
|
|
61
|
+
|
|
62
|
+
if (afterCleanup.length === 1 && afterCleanup[0].role === 'user') {
|
|
63
|
+
console.log(' ✓ Incomplete assistant message removed!\n');
|
|
64
|
+
} else {
|
|
65
|
+
console.log(' ✗ Cleanup failed!\n');
|
|
66
|
+
console.log(' Remaining messages:', JSON.stringify(afterCleanup, null, 2));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.log('3️⃣ Testing with complete messages (no tool_use)...');
|
|
70
|
+
|
|
71
|
+
// Add complete assistant message
|
|
72
|
+
(agent as any).messages.push({
|
|
73
|
+
role: 'assistant',
|
|
74
|
+
content: [{ type: 'text', text: 'Here is your answer.' }],
|
|
75
|
+
});
|
|
76
|
+
console.log(` Before cleanup: ${agent.getHistory().length} messages`);
|
|
77
|
+
|
|
78
|
+
agent.cleanupIncompleteMessages();
|
|
79
|
+
console.log(` After cleanup: ${agent.getHistory().length} messages`);
|
|
80
|
+
|
|
81
|
+
if (agent.getHistory().length === 2) {
|
|
82
|
+
console.log(' ✓ Complete messages preserved!\n');
|
|
83
|
+
} else {
|
|
84
|
+
console.log(' ✗ Complete message was incorrectly removed!\n');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('✅ All tests passed!\n');
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error('❌ Test failed:', error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Run the test
|
|
94
|
+
testInterruptCleanup();
|
package/package.json
CHANGED
package/src/agent/agent.ts
CHANGED
|
@@ -16,6 +16,16 @@ import { SessionManager } from '../session/index.js';
|
|
|
16
16
|
import { MemoryManager, type LoadedMemory } from '../memory/index.js';
|
|
17
17
|
import type { AgentConfig, AgentEvent } from './types.js';
|
|
18
18
|
import { buildSystemPromptForModel, debugPromptLoading } from '../prompts/index.js';
|
|
19
|
+
import type { Question, QuestionAnswer } from '../tools/types.js';
|
|
20
|
+
import {
|
|
21
|
+
getPlanModeManager,
|
|
22
|
+
type PlanModeManager,
|
|
23
|
+
type ModeType,
|
|
24
|
+
type AllowedPrompt,
|
|
25
|
+
} from '../planning/index.js';
|
|
26
|
+
|
|
27
|
+
// Type for askUser callback
|
|
28
|
+
export type AskUserCallback = (questions: Question[]) => Promise<QuestionAnswer[]>;
|
|
19
29
|
|
|
20
30
|
export class Agent {
|
|
21
31
|
private provider: LLMProvider;
|
|
@@ -23,10 +33,12 @@ export class Agent {
|
|
|
23
33
|
private permissions: PermissionManager;
|
|
24
34
|
private sessionManager: SessionManager;
|
|
25
35
|
private memoryManager: MemoryManager;
|
|
36
|
+
private planModeManager: PlanModeManager;
|
|
26
37
|
private config: AgentConfig;
|
|
27
38
|
private messages: Message[] = [];
|
|
28
39
|
private sessionId: string | null = null;
|
|
29
40
|
private loadedMemory: LoadedMemory | null = null;
|
|
41
|
+
private askUserCallback: AskUserCallback | null = null;
|
|
30
42
|
|
|
31
43
|
constructor(config: AgentConfig) {
|
|
32
44
|
this.config = {
|
|
@@ -43,6 +55,7 @@ export class Agent {
|
|
|
43
55
|
});
|
|
44
56
|
this.sessionManager = new SessionManager();
|
|
45
57
|
this.memoryManager = new MemoryManager();
|
|
58
|
+
this.planModeManager = getPlanModeManager();
|
|
46
59
|
}
|
|
47
60
|
|
|
48
61
|
/**
|
|
@@ -101,6 +114,14 @@ export class Agent {
|
|
|
101
114
|
return this.permissions;
|
|
102
115
|
}
|
|
103
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Set callback for AskUserQuestion tool
|
|
119
|
+
* This allows the CLI to handle user questioning
|
|
120
|
+
*/
|
|
121
|
+
setAskUserCallback(callback: AskUserCallback): void {
|
|
122
|
+
this.askUserCallback = callback;
|
|
123
|
+
}
|
|
124
|
+
|
|
104
125
|
/**
|
|
105
126
|
* Get memory manager for direct access
|
|
106
127
|
*/
|
|
@@ -124,6 +145,78 @@ export class Agent {
|
|
|
124
145
|
return this.loadedMemory;
|
|
125
146
|
}
|
|
126
147
|
|
|
148
|
+
// ============================================================================
|
|
149
|
+
// Plan Mode
|
|
150
|
+
// ============================================================================
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get plan mode manager for external access
|
|
154
|
+
*/
|
|
155
|
+
getPlanModeManager(): PlanModeManager {
|
|
156
|
+
return this.planModeManager;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Check if plan mode is active
|
|
161
|
+
*/
|
|
162
|
+
isPlanModeActive(): boolean {
|
|
163
|
+
return this.planModeManager.isActive();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Get current mode (build or plan)
|
|
168
|
+
*/
|
|
169
|
+
getCurrentMode(): ModeType {
|
|
170
|
+
return this.planModeManager.getCurrentMode();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Enter plan mode programmatically
|
|
175
|
+
*/
|
|
176
|
+
async enterPlanMode(taskDescription?: string): Promise<string> {
|
|
177
|
+
const { createPlanFile } = await import('../planning/index.js');
|
|
178
|
+
const cwd = this.config.cwd ?? process.cwd();
|
|
179
|
+
const planFile = await createPlanFile(cwd, taskDescription);
|
|
180
|
+
this.planModeManager.enter(planFile.path, taskDescription);
|
|
181
|
+
return planFile.path;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Exit plan mode
|
|
186
|
+
*/
|
|
187
|
+
exitPlanMode(approved: boolean = false): void {
|
|
188
|
+
if (approved) {
|
|
189
|
+
// Add allowed prompts from plan mode to permissions
|
|
190
|
+
const allowedPrompts = this.planModeManager.getRequestedPermissions();
|
|
191
|
+
if (allowedPrompts.length > 0) {
|
|
192
|
+
this.permissions.addAllowedPrompts(allowedPrompts);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.planModeManager.exit(approved);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Toggle plan mode
|
|
200
|
+
*/
|
|
201
|
+
async togglePlanMode(): Promise<void> {
|
|
202
|
+
if (this.planModeManager.isActive()) {
|
|
203
|
+
this.planModeManager.exit(false);
|
|
204
|
+
} else {
|
|
205
|
+
await this.enterPlanMode();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get plan mode requested permissions
|
|
211
|
+
*/
|
|
212
|
+
getPlanModePermissions(): AllowedPrompt[] {
|
|
213
|
+
return this.planModeManager.getRequestedPermissions();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Session Management
|
|
218
|
+
// ============================================================================
|
|
219
|
+
|
|
127
220
|
/**
|
|
128
221
|
* Get current session ID
|
|
129
222
|
*/
|
|
@@ -277,8 +370,8 @@ export class Agent {
|
|
|
277
370
|
while (turns < maxTurns) {
|
|
278
371
|
turns++;
|
|
279
372
|
|
|
280
|
-
// Get tool definitions
|
|
281
|
-
const toolDefs = this.registry.
|
|
373
|
+
// Get tool definitions (filtered by plan mode if active)
|
|
374
|
+
const toolDefs = this.registry.getFilteredDefinitions(this.config.tools);
|
|
282
375
|
|
|
283
376
|
// Call LLM
|
|
284
377
|
let response;
|
|
@@ -328,7 +421,7 @@ export class Agent {
|
|
|
328
421
|
await this.sessionManager.addMessage({ role: 'assistant', content: response.content });
|
|
329
422
|
|
|
330
423
|
if (response.stopReason !== 'tool_use' || toolCalls.length === 0) {
|
|
331
|
-
yield { type: 'done', text: textContent };
|
|
424
|
+
yield { type: 'done', text: textContent, usage: response.usage, cost: response.cost };
|
|
332
425
|
return;
|
|
333
426
|
}
|
|
334
427
|
|
|
@@ -336,12 +429,18 @@ export class Agent {
|
|
|
336
429
|
const toolResults: ToolResultContent[] = [];
|
|
337
430
|
const cwd = this.config.cwd ?? process.cwd();
|
|
338
431
|
|
|
432
|
+
// Build tool context with askUser callback
|
|
433
|
+
const toolContext = {
|
|
434
|
+
cwd,
|
|
435
|
+
askUser: this.askUserCallback ?? undefined,
|
|
436
|
+
};
|
|
437
|
+
|
|
339
438
|
for (const call of toolCalls) {
|
|
340
439
|
yield { type: 'tool_start', id: call.id, name: call.name, input: call.input };
|
|
341
440
|
|
|
342
441
|
const allowed = await this.permissions.requestPermission(call.name, call.input);
|
|
343
442
|
const result = allowed
|
|
344
|
-
? await this.registry.execute(call.name, call.input,
|
|
443
|
+
? await this.registry.execute(call.name, call.input, toolContext)
|
|
345
444
|
: { success: false, output: '', error: 'Permission denied by user' };
|
|
346
445
|
|
|
347
446
|
yield { type: 'tool_result', id: call.id, name: call.name, result };
|
|
@@ -369,6 +468,33 @@ export class Agent {
|
|
|
369
468
|
this.sessionManager.clearMessages();
|
|
370
469
|
}
|
|
371
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Clean up incomplete tool use messages (after interruption)
|
|
473
|
+
* Removes the last assistant message if it contains tool_use without corresponding tool_result
|
|
474
|
+
*/
|
|
475
|
+
cleanupIncompleteMessages(): void {
|
|
476
|
+
if (this.messages.length === 0) return;
|
|
477
|
+
|
|
478
|
+
const lastMessage = this.messages[this.messages.length - 1];
|
|
479
|
+
|
|
480
|
+
// Check if last message is an assistant message with tool_use
|
|
481
|
+
if (lastMessage.role === 'assistant' && Array.isArray(lastMessage.content)) {
|
|
482
|
+
const hasToolUse = lastMessage.content.some((c) => c.type === 'tool_use');
|
|
483
|
+
|
|
484
|
+
if (hasToolUse) {
|
|
485
|
+
// Remove the incomplete assistant message
|
|
486
|
+
this.messages.pop();
|
|
487
|
+
|
|
488
|
+
// Also remove from session manager
|
|
489
|
+
// Note: SessionManager should have corresponding cleanup method
|
|
490
|
+
const messages = this.sessionManager.getMessages();
|
|
491
|
+
if (messages.length > 0 && messages[messages.length - 1].role === 'assistant') {
|
|
492
|
+
this.sessionManager.removeLastMessage();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
372
498
|
/**
|
|
373
499
|
* Get conversation history
|
|
374
500
|
*/
|
package/src/agent/index.ts
CHANGED
package/src/agent/types.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { PermissionConfig } from '../permissions/types.js';
|
|
6
|
+
import type { CostEstimate } from '../pricing/types.js';
|
|
6
7
|
|
|
7
8
|
export interface AgentConfig {
|
|
8
9
|
provider: 'openai' | 'anthropic' | 'gemini' | 'vertex-ai';
|
|
@@ -59,6 +60,22 @@ export interface AgentEventError {
|
|
|
59
60
|
export interface AgentEventDone {
|
|
60
61
|
type: 'done';
|
|
61
62
|
text: string;
|
|
63
|
+
usage?: {
|
|
64
|
+
inputTokens: number;
|
|
65
|
+
outputTokens: number;
|
|
66
|
+
};
|
|
67
|
+
cost?: CostEstimate;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface AgentEventAskUser {
|
|
71
|
+
type: 'ask_user';
|
|
72
|
+
id: string;
|
|
73
|
+
questions: Array<{
|
|
74
|
+
question: string;
|
|
75
|
+
header: string;
|
|
76
|
+
options: Array<{ label: string; description: string }>;
|
|
77
|
+
multiSelect: boolean;
|
|
78
|
+
}>;
|
|
62
79
|
}
|
|
63
80
|
|
|
64
81
|
export type AgentEvent =
|
|
@@ -67,4 +84,5 @@ export type AgentEvent =
|
|
|
67
84
|
| AgentEventToolResult
|
|
68
85
|
| AgentEventThinking
|
|
69
86
|
| AgentEventError
|
|
70
|
-
| AgentEventDone
|
|
87
|
+
| AgentEventDone
|
|
88
|
+
| AgentEventAskUser;
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint Manager - Core checkpointing logic
|
|
3
|
+
*
|
|
4
|
+
* Tracks file changes and provides rewind capabilities.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as fs from 'fs/promises';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import type {
|
|
10
|
+
FileCheckpoint,
|
|
11
|
+
CheckpointSession,
|
|
12
|
+
RewindOptions,
|
|
13
|
+
RewindResult,
|
|
14
|
+
CheckpointSummary,
|
|
15
|
+
RecordChangeInput,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Generates a unique ID for checkpoints
|
|
20
|
+
*/
|
|
21
|
+
function generateId(): string {
|
|
22
|
+
return `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* CheckpointManager manages file change tracking and rewind operations.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* const manager = new CheckpointManager('session-123');
|
|
30
|
+
* manager.recordChange({ path: '/path/to/file', changeType: 'modify', ... });
|
|
31
|
+
* await manager.rewind({ all: true });
|
|
32
|
+
*/
|
|
33
|
+
export class CheckpointManager {
|
|
34
|
+
private session: CheckpointSession;
|
|
35
|
+
|
|
36
|
+
constructor(sessionId: string = 'default') {
|
|
37
|
+
this.session = {
|
|
38
|
+
sessionId,
|
|
39
|
+
checkpoints: [],
|
|
40
|
+
createdAt: new Date(),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Get the session ID
|
|
46
|
+
*/
|
|
47
|
+
getSessionId(): string {
|
|
48
|
+
return this.session.sessionId;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Record a file change as a checkpoint
|
|
53
|
+
*/
|
|
54
|
+
recordChange(input: RecordChangeInput): FileCheckpoint {
|
|
55
|
+
const checkpoint: FileCheckpoint = {
|
|
56
|
+
id: generateId(),
|
|
57
|
+
path: input.path,
|
|
58
|
+
changeType: input.changeType,
|
|
59
|
+
timestamp: new Date(),
|
|
60
|
+
previousContent: input.previousContent,
|
|
61
|
+
newContent: input.newContent,
|
|
62
|
+
toolName: input.toolName,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
this.session.checkpoints.push(checkpoint);
|
|
66
|
+
return checkpoint;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get all checkpoints
|
|
71
|
+
*/
|
|
72
|
+
getCheckpoints(): FileCheckpoint[] {
|
|
73
|
+
return [...this.session.checkpoints];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get checkpoints for a specific file
|
|
78
|
+
*/
|
|
79
|
+
getFileHistory(filePath: string): FileCheckpoint[] {
|
|
80
|
+
return this.session.checkpoints.filter((cp) => cp.path === filePath);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get a summary of all changes
|
|
85
|
+
*/
|
|
86
|
+
getSummary(): CheckpointSummary {
|
|
87
|
+
const summary: CheckpointSummary = {
|
|
88
|
+
created: 0,
|
|
89
|
+
modified: 0,
|
|
90
|
+
deleted: 0,
|
|
91
|
+
total: this.session.checkpoints.length,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
for (const cp of this.session.checkpoints) {
|
|
95
|
+
switch (cp.changeType) {
|
|
96
|
+
case 'create':
|
|
97
|
+
summary.created++;
|
|
98
|
+
break;
|
|
99
|
+
case 'modify':
|
|
100
|
+
summary.modified++;
|
|
101
|
+
break;
|
|
102
|
+
case 'delete':
|
|
103
|
+
summary.deleted++;
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return summary;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if there are any checkpoints
|
|
113
|
+
*/
|
|
114
|
+
hasCheckpoints(): boolean {
|
|
115
|
+
return this.session.checkpoints.length > 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the number of checkpoints
|
|
120
|
+
*/
|
|
121
|
+
getCheckpointCount(): number {
|
|
122
|
+
return this.session.checkpoints.length;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Rewind changes based on options
|
|
127
|
+
*/
|
|
128
|
+
async rewind(options: RewindOptions): Promise<RewindResult> {
|
|
129
|
+
const result: RewindResult = {
|
|
130
|
+
success: true,
|
|
131
|
+
revertedFiles: [],
|
|
132
|
+
errors: [],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// Determine which checkpoints to rewind
|
|
136
|
+
let checkpointsToRewind: FileCheckpoint[] = [];
|
|
137
|
+
|
|
138
|
+
if (options.checkpointId) {
|
|
139
|
+
// Rewind specific checkpoint
|
|
140
|
+
const cp = this.session.checkpoints.find((c) => c.id === options.checkpointId);
|
|
141
|
+
if (cp) {
|
|
142
|
+
checkpointsToRewind = [cp];
|
|
143
|
+
}
|
|
144
|
+
} else if (options.path) {
|
|
145
|
+
// Rewind all changes to a specific file (in reverse order)
|
|
146
|
+
checkpointsToRewind = this.session.checkpoints
|
|
147
|
+
.filter((c) => c.path === options.path)
|
|
148
|
+
.reverse();
|
|
149
|
+
} else if (options.count) {
|
|
150
|
+
// Rewind last N changes (in reverse order)
|
|
151
|
+
checkpointsToRewind = this.session.checkpoints.slice(-options.count).reverse();
|
|
152
|
+
} else if (options.all) {
|
|
153
|
+
// Rewind all changes (in reverse order)
|
|
154
|
+
checkpointsToRewind = [...this.session.checkpoints].reverse();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Apply reverts
|
|
158
|
+
for (const checkpoint of checkpointsToRewind) {
|
|
159
|
+
try {
|
|
160
|
+
await this.revertCheckpoint(checkpoint);
|
|
161
|
+
result.revertedFiles.push({
|
|
162
|
+
path: checkpoint.path,
|
|
163
|
+
action: this.getRevertAction(checkpoint),
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Remove the checkpoint from session
|
|
167
|
+
const index = this.session.checkpoints.findIndex((c) => c.id === checkpoint.id);
|
|
168
|
+
if (index !== -1) {
|
|
169
|
+
this.session.checkpoints.splice(index, 1);
|
|
170
|
+
}
|
|
171
|
+
} catch (error) {
|
|
172
|
+
result.success = false;
|
|
173
|
+
result.errors.push({
|
|
174
|
+
path: checkpoint.path,
|
|
175
|
+
error: error instanceof Error ? error.message : String(error),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return result;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Revert a single checkpoint
|
|
185
|
+
*/
|
|
186
|
+
private async revertCheckpoint(checkpoint: FileCheckpoint): Promise<void> {
|
|
187
|
+
switch (checkpoint.changeType) {
|
|
188
|
+
case 'create':
|
|
189
|
+
// File was created, delete it to revert
|
|
190
|
+
await fs.unlink(checkpoint.path);
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case 'modify':
|
|
194
|
+
// File was modified, restore previous content
|
|
195
|
+
if (checkpoint.previousContent !== null) {
|
|
196
|
+
await fs.writeFile(checkpoint.path, checkpoint.previousContent, 'utf-8');
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
|
|
200
|
+
case 'delete':
|
|
201
|
+
// File was deleted, recreate it with previous content
|
|
202
|
+
if (checkpoint.previousContent !== null) {
|
|
203
|
+
await fs.mkdir(path.dirname(checkpoint.path), { recursive: true });
|
|
204
|
+
await fs.writeFile(checkpoint.path, checkpoint.previousContent, 'utf-8');
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get the action that will be taken to revert a checkpoint
|
|
212
|
+
*/
|
|
213
|
+
private getRevertAction(checkpoint: FileCheckpoint): 'restored' | 'deleted' | 'recreated' {
|
|
214
|
+
switch (checkpoint.changeType) {
|
|
215
|
+
case 'create':
|
|
216
|
+
return 'deleted';
|
|
217
|
+
case 'modify':
|
|
218
|
+
return 'restored';
|
|
219
|
+
case 'delete':
|
|
220
|
+
return 'recreated';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Clear all checkpoints
|
|
226
|
+
*/
|
|
227
|
+
clearCheckpoints(): void {
|
|
228
|
+
this.session.checkpoints = [];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Format checkpoints for display
|
|
233
|
+
*/
|
|
234
|
+
formatCheckpointList(includeUsage: boolean = false): string {
|
|
235
|
+
if (this.session.checkpoints.length === 0) {
|
|
236
|
+
return 'No file changes in this session.';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const lines: string[] = [];
|
|
240
|
+
|
|
241
|
+
this.session.checkpoints.forEach((cp, index) => {
|
|
242
|
+
const timeAgo = this.formatTimeAgo(cp.timestamp);
|
|
243
|
+
const action = this.formatChangeType(cp.changeType);
|
|
244
|
+
const fileName = cp.path.split('/').pop() || cp.path;
|
|
245
|
+
lines.push(` [${index + 1}] ${timeAgo.padEnd(8)} ${fileName.padEnd(30)} (${action})`);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const summary = this.getSummary();
|
|
249
|
+
lines.push('');
|
|
250
|
+
lines.push(
|
|
251
|
+
`Total: ${summary.created} created, ${summary.modified} modified, ${summary.deleted} deleted`
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
if (includeUsage) {
|
|
255
|
+
lines.push('');
|
|
256
|
+
lines.push('Usage: /rewind [n] to revert change #n, /rewind all to revert all');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return lines.join('\n');
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Format a change type for display
|
|
264
|
+
*/
|
|
265
|
+
private formatChangeType(changeType: string): string {
|
|
266
|
+
switch (changeType) {
|
|
267
|
+
case 'create':
|
|
268
|
+
return 'created';
|
|
269
|
+
case 'modify':
|
|
270
|
+
return 'modified';
|
|
271
|
+
case 'delete':
|
|
272
|
+
return 'deleted';
|
|
273
|
+
default:
|
|
274
|
+
return changeType;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Format a timestamp as relative time
|
|
280
|
+
*/
|
|
281
|
+
private formatTimeAgo(date: Date): string {
|
|
282
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
|
|
283
|
+
|
|
284
|
+
if (seconds < 60) {
|
|
285
|
+
return `${seconds}s ago`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const minutes = Math.floor(seconds / 60);
|
|
289
|
+
if (minutes < 60) {
|
|
290
|
+
return `${minutes}m ago`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const hours = Math.floor(minutes / 60);
|
|
294
|
+
return `${hours}h ago`;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Singleton Instance
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
let globalCheckpointManager: CheckpointManager | null = null;
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Get the global checkpoint manager instance
|
|
306
|
+
*/
|
|
307
|
+
export function getCheckpointManager(): CheckpointManager {
|
|
308
|
+
if (!globalCheckpointManager) {
|
|
309
|
+
globalCheckpointManager = new CheckpointManager();
|
|
310
|
+
}
|
|
311
|
+
return globalCheckpointManager;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Initialize checkpoint manager with a session ID
|
|
316
|
+
*/
|
|
317
|
+
export function initCheckpointManager(sessionId: string): CheckpointManager {
|
|
318
|
+
globalCheckpointManager = new CheckpointManager(sessionId);
|
|
319
|
+
return globalCheckpointManager;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Reset the global checkpoint manager (for testing)
|
|
324
|
+
*/
|
|
325
|
+
export function resetCheckpointManager(): void {
|
|
326
|
+
globalCheckpointManager = null;
|
|
327
|
+
}
|