grov 0.1.2 → 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/README.md +66 -87
- package/dist/cli.js +23 -37
- package/dist/commands/capture.js +1 -1
- package/dist/commands/disable.d.ts +1 -0
- package/dist/commands/disable.js +14 -0
- package/dist/commands/drift-test.js +56 -68
- package/dist/commands/init.js +29 -17
- package/dist/commands/proxy-status.d.ts +1 -0
- package/dist/commands/proxy-status.js +32 -0
- package/dist/commands/unregister.js +7 -1
- package/dist/lib/correction-builder-proxy.d.ts +16 -0
- package/dist/lib/correction-builder-proxy.js +125 -0
- package/dist/lib/correction-builder.js +1 -1
- package/dist/lib/drift-checker-proxy.d.ts +63 -0
- package/dist/lib/drift-checker-proxy.js +373 -0
- package/dist/lib/drift-checker.js +1 -1
- package/dist/lib/hooks.d.ts +11 -0
- package/dist/lib/hooks.js +33 -0
- package/dist/lib/llm-extractor.d.ts +60 -11
- package/dist/lib/llm-extractor.js +419 -98
- package/dist/lib/settings.d.ts +19 -0
- package/dist/lib/settings.js +63 -0
- package/dist/lib/store.d.ts +201 -43
- package/dist/lib/store.js +653 -90
- package/dist/proxy/action-parser.d.ts +58 -0
- package/dist/proxy/action-parser.js +196 -0
- package/dist/proxy/config.d.ts +26 -0
- package/dist/proxy/config.js +67 -0
- package/dist/proxy/forwarder.d.ts +24 -0
- package/dist/proxy/forwarder.js +119 -0
- package/dist/proxy/index.d.ts +1 -0
- package/dist/proxy/index.js +30 -0
- package/dist/proxy/request-processor.d.ts +12 -0
- package/dist/proxy/request-processor.js +94 -0
- package/dist/proxy/response-processor.d.ts +14 -0
- package/dist/proxy/response-processor.js +128 -0
- package/dist/proxy/server.d.ts +9 -0
- package/dist/proxy/server.js +911 -0
- package/package.json +8 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ParsedSession } from './jsonl-parser.js';
|
|
2
|
-
import type { TaskStatus } from './store.js';
|
|
2
|
+
import type { TaskStatus, SessionState, StepRecord } from './store.js';
|
|
3
3
|
export interface ExtractedReasoning {
|
|
4
4
|
task: string;
|
|
5
5
|
goal: string;
|
|
@@ -17,6 +17,23 @@ export interface ExtractedReasoning {
|
|
|
17
17
|
* Check if LLM extraction is available (OpenAI API key set)
|
|
18
18
|
*/
|
|
19
19
|
export declare function isLLMAvailable(): boolean;
|
|
20
|
+
export interface ExtractedIntent {
|
|
21
|
+
goal: string;
|
|
22
|
+
expected_scope: string[];
|
|
23
|
+
constraints: string[];
|
|
24
|
+
success_criteria?: string[];
|
|
25
|
+
keywords: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Extract intent from first user prompt using Haiku
|
|
29
|
+
* Called once at session start to populate session_states
|
|
30
|
+
* Falls back to basic extraction if API unavailable (for hook compatibility)
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractIntent(firstPrompt: string): Promise<ExtractedIntent>;
|
|
33
|
+
/**
|
|
34
|
+
* Check if intent extraction is available
|
|
35
|
+
*/
|
|
36
|
+
export declare function isIntentExtractionAvailable(): boolean;
|
|
20
37
|
/**
|
|
21
38
|
* Check if Anthropic API is available (for drift detection)
|
|
22
39
|
*/
|
|
@@ -34,17 +51,49 @@ export declare function extractReasoning(session: ParsedSession): Promise<Extrac
|
|
|
34
51
|
*/
|
|
35
52
|
export declare function classifyTaskStatus(session: ParsedSession): Promise<TaskStatus>;
|
|
36
53
|
/**
|
|
37
|
-
*
|
|
54
|
+
* Check if session summary generation is available
|
|
38
55
|
*/
|
|
39
|
-
export
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
export declare function isSummaryAvailable(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Generate session summary for CLEAR operation
|
|
59
|
+
* Reference: plan_proxy_local.md Section 2.3, 4.5
|
|
60
|
+
*/
|
|
61
|
+
export declare function generateSessionSummary(sessionState: SessionState, steps: StepRecord[]): Promise<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Task analysis result from Haiku
|
|
64
|
+
*/
|
|
65
|
+
export interface TaskAnalysis {
|
|
66
|
+
action: 'continue' | 'new_task' | 'subtask' | 'parallel_task' | 'task_complete' | 'subtask_complete';
|
|
67
|
+
topic_match?: 'YES' | 'NO';
|
|
68
|
+
task_id: string;
|
|
69
|
+
current_goal: string;
|
|
70
|
+
parent_task_id?: string;
|
|
71
|
+
reasoning: string;
|
|
72
|
+
step_reasoning?: string;
|
|
45
73
|
}
|
|
46
74
|
/**
|
|
47
|
-
*
|
|
48
|
-
|
|
75
|
+
* Check if task analysis is available
|
|
76
|
+
*/
|
|
77
|
+
export declare function isTaskAnalysisAvailable(): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Analyze task context to determine task status
|
|
80
|
+
* Called after each main model response to orchestrate sessions
|
|
81
|
+
* Also compresses reasoning for steps if assistantResponse > 1000 chars
|
|
82
|
+
*/
|
|
83
|
+
export declare function analyzeTaskContext(currentSession: SessionState | null, latestUserMessage: string, recentSteps: StepRecord[], assistantResponse: string): Promise<TaskAnalysis>;
|
|
84
|
+
export interface ExtractedReasoningAndDecisions {
|
|
85
|
+
reasoning_trace: string[];
|
|
86
|
+
decisions: Array<{
|
|
87
|
+
choice: string;
|
|
88
|
+
reason: string;
|
|
89
|
+
}>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Check if reasoning extraction is available
|
|
93
|
+
*/
|
|
94
|
+
export declare function isReasoningExtractionAvailable(): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Extract reasoning trace and decisions from steps
|
|
97
|
+
* Called at task_complete to populate team memory with rich context
|
|
49
98
|
*/
|
|
50
|
-
export declare function
|
|
99
|
+
export declare function extractReasoningAndDecisions(stepsReasoning: string[], originalGoal: string): Promise<ExtractedReasoningAndDecisions>;
|
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
// and Anthropic Claude Haiku for drift detection
|
|
3
3
|
import OpenAI from 'openai';
|
|
4
4
|
import Anthropic from '@anthropic-ai/sdk';
|
|
5
|
+
import { config } from 'dotenv';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { existsSync } from 'fs';
|
|
5
9
|
import { debugLLM } from './debug.js';
|
|
6
10
|
import { truncate } from './utils.js';
|
|
11
|
+
// Load ~/.grov/.env as fallback for API key
|
|
12
|
+
// This allows users to store their API key in a safe location outside any repo
|
|
13
|
+
const grovEnvPath = join(homedir(), '.grov', '.env');
|
|
14
|
+
if (existsSync(grovEnvPath)) {
|
|
15
|
+
config({ path: grovEnvPath });
|
|
16
|
+
}
|
|
7
17
|
let client = null;
|
|
8
18
|
let anthropicClient = null;
|
|
9
19
|
/**
|
|
@@ -39,6 +49,133 @@ function getAnthropicClient() {
|
|
|
39
49
|
export function isLLMAvailable() {
|
|
40
50
|
return !!process.env.OPENAI_API_KEY;
|
|
41
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Extract intent from first user prompt using Haiku
|
|
54
|
+
* Called once at session start to populate session_states
|
|
55
|
+
* Falls back to basic extraction if API unavailable (for hook compatibility)
|
|
56
|
+
*/
|
|
57
|
+
export async function extractIntent(firstPrompt) {
|
|
58
|
+
// Check availability first - allows hook to work without API key
|
|
59
|
+
if (!isIntentExtractionAvailable()) {
|
|
60
|
+
return createFallbackIntent(firstPrompt);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const client = getAnthropicClient();
|
|
64
|
+
const prompt = `Analyze this user request and extract structured intent for a coding assistant session.
|
|
65
|
+
|
|
66
|
+
USER REQUEST:
|
|
67
|
+
${firstPrompt.substring(0, 2000)}
|
|
68
|
+
|
|
69
|
+
Extract as JSON:
|
|
70
|
+
{
|
|
71
|
+
"goal": "The main objective in 1-2 sentences",
|
|
72
|
+
"expected_scope": ["list", "of", "files/folders", "likely", "to", "be", "modified"],
|
|
73
|
+
"constraints": ["EXPLICIT restrictions from the user - see examples below"],
|
|
74
|
+
"success_criteria": ["How to know when the task is complete"],
|
|
75
|
+
"keywords": ["relevant", "technical", "terms"]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
═══════════════════════════════════════════════════════════════
|
|
79
|
+
CONSTRAINTS EXTRACTION - BE VERY THOROUGH
|
|
80
|
+
═══════════════════════════════════════════════════════════════
|
|
81
|
+
|
|
82
|
+
Look for NEGATIVE constraints (things NOT to do):
|
|
83
|
+
- "NU modifica" / "DON'T modify" / "NEVER change" / "don't touch"
|
|
84
|
+
- "NU rula" / "DON'T run" / "NO commands" / "don't execute"
|
|
85
|
+
- "fără X" / "without X" / "except X" / "not including"
|
|
86
|
+
- "nu scrie cod" / "don't write code" / "just plan"
|
|
87
|
+
|
|
88
|
+
Look for POSITIVE constraints (things MUST do / ONLY do):
|
|
89
|
+
- "ONLY modify X" / "DOAR în X" / "only in folder Y"
|
|
90
|
+
- "must use Y" / "trebuie să folosești Y"
|
|
91
|
+
- "keep it simple" / "no external dependencies"
|
|
92
|
+
- "use TypeScript" / "must be async"
|
|
93
|
+
|
|
94
|
+
EXAMPLES:
|
|
95
|
+
Input: "Fix bug in auth. NU modifica nimic in afara de sandbox/, NU rula comenzi."
|
|
96
|
+
Output constraints: ["DO NOT modify files outside sandbox/", "DO NOT run commands"]
|
|
97
|
+
|
|
98
|
+
Input: "Add feature X. Only use standard library, keep backward compatible."
|
|
99
|
+
Output constraints: ["ONLY use standard library", "Keep backward compatible"]
|
|
100
|
+
|
|
101
|
+
Input: "Analyze code and create plan. Nu scrie cod inca, doar planifica."
|
|
102
|
+
Output constraints: ["DO NOT write code yet", "Only create plan/analysis"]
|
|
103
|
+
|
|
104
|
+
For expected_scope:
|
|
105
|
+
- Include file patterns (e.g., "src/auth/", "*.test.ts", "sandbox/")
|
|
106
|
+
- Include component/module names mentioned
|
|
107
|
+
- Be conservative - only include clearly relevant areas
|
|
108
|
+
|
|
109
|
+
RESPONSE RULES:
|
|
110
|
+
- English only (translate Romanian/other languages to English)
|
|
111
|
+
- No emojis
|
|
112
|
+
- Valid JSON only
|
|
113
|
+
- If no constraints found, return empty array []`;
|
|
114
|
+
const response = await client.messages.create({
|
|
115
|
+
model: 'claude-haiku-4-5-20251001',
|
|
116
|
+
max_tokens: 500,
|
|
117
|
+
messages: [{ role: 'user', content: prompt }],
|
|
118
|
+
});
|
|
119
|
+
const content = response.content?.[0];
|
|
120
|
+
if (!content || content.type !== 'text') {
|
|
121
|
+
return createFallbackIntent(firstPrompt);
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const jsonMatch = content.text.match(/\{[\s\S]*\}/);
|
|
125
|
+
if (!jsonMatch) {
|
|
126
|
+
return createFallbackIntent(firstPrompt);
|
|
127
|
+
}
|
|
128
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
129
|
+
return {
|
|
130
|
+
goal: typeof parsed.goal === 'string' ? parsed.goal : firstPrompt.substring(0, 200),
|
|
131
|
+
expected_scope: Array.isArray(parsed.expected_scope)
|
|
132
|
+
? parsed.expected_scope.filter((s) => typeof s === 'string')
|
|
133
|
+
: [],
|
|
134
|
+
constraints: Array.isArray(parsed.constraints)
|
|
135
|
+
? parsed.constraints.filter((c) => typeof c === 'string')
|
|
136
|
+
: [],
|
|
137
|
+
success_criteria: Array.isArray(parsed.success_criteria)
|
|
138
|
+
? parsed.success_criteria.filter((s) => typeof s === 'string')
|
|
139
|
+
: [],
|
|
140
|
+
keywords: Array.isArray(parsed.keywords)
|
|
141
|
+
? parsed.keywords.filter((k) => typeof k === 'string')
|
|
142
|
+
: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return createFallbackIntent(firstPrompt);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
// Outer catch - API errors, network issues, etc.
|
|
151
|
+
return createFallbackIntent(firstPrompt);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Fallback intent extraction without LLM
|
|
156
|
+
*/
|
|
157
|
+
function createFallbackIntent(prompt) {
|
|
158
|
+
// Basic keyword extraction
|
|
159
|
+
const words = prompt.toLowerCase().split(/\s+/);
|
|
160
|
+
const techKeywords = words.filter(w => w.length > 3 &&
|
|
161
|
+
/^[a-z]+$/.test(w) &&
|
|
162
|
+
!['this', 'that', 'with', 'from', 'have', 'will', 'would', 'could', 'should'].includes(w));
|
|
163
|
+
// Extract file patterns
|
|
164
|
+
const filePatterns = prompt.match(/[\w\/.-]+\.(ts|js|tsx|jsx|py|go|rs|java|css|html|md)/g) || [];
|
|
165
|
+
return {
|
|
166
|
+
goal: prompt.substring(0, 200),
|
|
167
|
+
expected_scope: [...new Set(filePatterns)].slice(0, 5),
|
|
168
|
+
constraints: [],
|
|
169
|
+
success_criteria: [],
|
|
170
|
+
keywords: [...new Set(techKeywords)].slice(0, 10),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Check if intent extraction is available
|
|
175
|
+
*/
|
|
176
|
+
export function isIntentExtractionAvailable() {
|
|
177
|
+
return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
|
|
178
|
+
}
|
|
42
179
|
/**
|
|
43
180
|
* Check if Anthropic API is available (for drift detection)
|
|
44
181
|
*/
|
|
@@ -75,22 +212,37 @@ ${sessionSummary}
|
|
|
75
212
|
|
|
76
213
|
Extract the following as JSON:
|
|
77
214
|
{
|
|
78
|
-
"task": "Brief description
|
|
79
|
-
"goal": "The underlying
|
|
80
|
-
"reasoning_trace": [
|
|
81
|
-
|
|
82
|
-
|
|
215
|
+
"task": "Brief description (1 sentence)",
|
|
216
|
+
"goal": "The underlying problem being solved",
|
|
217
|
+
"reasoning_trace": [
|
|
218
|
+
"Be SPECIFIC: include file names, function names, line numbers when relevant",
|
|
219
|
+
"Format: '[Action] [target] to/for [purpose]'",
|
|
220
|
+
"Example: 'Read auth.ts:47 to understand token refresh logic'",
|
|
221
|
+
"Example: 'Fixed null check in validateToken() - was causing silent failures'",
|
|
222
|
+
"NOT: 'Investigated auth' or 'Fixed bug'"
|
|
223
|
+
],
|
|
224
|
+
"decisions": [{"choice": "What was decided", "reason": "Why this over alternatives"}],
|
|
225
|
+
"constraints": ["Discovered limitations, rate limits, incompatibilities"],
|
|
83
226
|
"status": "complete|partial|question|abandoned",
|
|
84
227
|
"tags": ["relevant", "domain", "tags"]
|
|
85
228
|
}
|
|
86
229
|
|
|
230
|
+
IMPORTANT for reasoning_trace:
|
|
231
|
+
- Each entry should be ACTIONABLE information for future developers
|
|
232
|
+
- Include specific file:line references when possible
|
|
233
|
+
- Explain WHY not just WHAT (e.g., "Chose JWT over sessions because stateless scales better")
|
|
234
|
+
- Bad: "Fixed the bug" / Good: "Fixed race condition in UserService.save() - was missing await"
|
|
235
|
+
|
|
87
236
|
Status definitions:
|
|
88
237
|
- "complete": Task was finished, implementation done
|
|
89
238
|
- "partial": Work started but not finished
|
|
90
239
|
- "question": Claude asked a question and is waiting for user response
|
|
91
240
|
- "abandoned": User interrupted or moved to different topic
|
|
92
241
|
|
|
93
|
-
|
|
242
|
+
RESPONSE RULES:
|
|
243
|
+
- English only (translate if input is in other language)
|
|
244
|
+
- No emojis
|
|
245
|
+
- Valid JSON only`
|
|
94
246
|
}
|
|
95
247
|
]
|
|
96
248
|
});
|
|
@@ -290,119 +442,288 @@ function validateStatus(status) {
|
|
|
290
442
|
}
|
|
291
443
|
return 'partial'; // Default
|
|
292
444
|
}
|
|
445
|
+
// ============================================
|
|
446
|
+
// SESSION SUMMARY FOR CLEAR OPERATION
|
|
447
|
+
// Reference: plan_proxy_local.md Section 2.3, 4.5
|
|
448
|
+
// ============================================
|
|
293
449
|
/**
|
|
294
|
-
*
|
|
295
|
-
* Falls back to basic extraction if API unavailable
|
|
450
|
+
* Check if session summary generation is available
|
|
296
451
|
*/
|
|
297
|
-
export
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
452
|
+
export function isSummaryAvailable() {
|
|
453
|
+
return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Generate session summary for CLEAR operation
|
|
457
|
+
* Reference: plan_proxy_local.md Section 2.3, 4.5
|
|
458
|
+
*/
|
|
459
|
+
export async function generateSessionSummary(sessionState, steps) {
|
|
460
|
+
const client = getAnthropicClient();
|
|
461
|
+
const stepsText = steps
|
|
462
|
+
.filter(s => s.is_validated)
|
|
463
|
+
.slice(-20)
|
|
464
|
+
.map(step => {
|
|
465
|
+
let desc = `- ${step.action_type}`;
|
|
466
|
+
if (step.files.length > 0) {
|
|
467
|
+
desc += `: ${step.files.join(', ')}`;
|
|
302
468
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
return extractIntentBasic(prompt);
|
|
469
|
+
if (step.command) {
|
|
470
|
+
desc += ` (${step.command.substring(0, 50)})`;
|
|
306
471
|
}
|
|
472
|
+
return desc;
|
|
473
|
+
})
|
|
474
|
+
.join('\n');
|
|
475
|
+
const prompt = `Create a concise summary of this coding session for context continuation.
|
|
476
|
+
|
|
477
|
+
ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
|
|
478
|
+
|
|
479
|
+
EXPECTED SCOPE: ${sessionState.expected_scope.join(', ') || 'Not specified'}
|
|
480
|
+
|
|
481
|
+
CONSTRAINTS: ${sessionState.constraints.join(', ') || 'None'}
|
|
482
|
+
|
|
483
|
+
ACTIONS TAKEN:
|
|
484
|
+
${stepsText || 'No actions recorded'}
|
|
485
|
+
|
|
486
|
+
Create a summary with these sections (keep total under 500 words):
|
|
487
|
+
1. ORIGINAL GOAL: (1 sentence)
|
|
488
|
+
2. PROGRESS: (2-3 bullet points of what was accomplished)
|
|
489
|
+
3. KEY DECISIONS: (any important choices made)
|
|
490
|
+
4. FILES MODIFIED: (list of files)
|
|
491
|
+
5. CURRENT STATE: (where the work left off)
|
|
492
|
+
6. NEXT STEPS: (recommended next actions)
|
|
493
|
+
|
|
494
|
+
Format as plain text, not JSON.`;
|
|
495
|
+
const response = await client.messages.create({
|
|
496
|
+
model: 'claude-haiku-4-5-20251001',
|
|
497
|
+
max_tokens: 800,
|
|
498
|
+
messages: [{ role: 'user', content: prompt }],
|
|
499
|
+
});
|
|
500
|
+
const content = response.content?.[0];
|
|
501
|
+
if (!content || content.type !== 'text') {
|
|
502
|
+
return createFallbackSummary(sessionState, steps);
|
|
307
503
|
}
|
|
308
|
-
|
|
309
|
-
|
|
504
|
+
return `PREVIOUS SESSION CONTEXT (auto-generated after context limit):
|
|
505
|
+
|
|
506
|
+
${content.text}`;
|
|
310
507
|
}
|
|
311
508
|
/**
|
|
312
|
-
*
|
|
509
|
+
* Create fallback summary without LLM
|
|
313
510
|
*/
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
const response = await anthropic.messages.create({
|
|
318
|
-
model,
|
|
319
|
-
max_tokens: 1024,
|
|
320
|
-
messages: [
|
|
321
|
-
{
|
|
322
|
-
role: 'user',
|
|
323
|
-
content: `Analyze this user prompt and extract the task intent. Return ONLY valid JSON, no explanation.
|
|
511
|
+
function createFallbackSummary(sessionState, steps) {
|
|
512
|
+
const files = [...new Set(steps.flatMap(s => s.files))];
|
|
513
|
+
return `PREVIOUS SESSION CONTEXT (auto-generated after context limit):
|
|
324
514
|
|
|
325
|
-
|
|
326
|
-
${prompt}
|
|
515
|
+
ORIGINAL GOAL: ${sessionState.original_goal || 'Not specified'}
|
|
327
516
|
|
|
328
|
-
|
|
517
|
+
PROGRESS: ${steps.length} actions taken
|
|
518
|
+
|
|
519
|
+
FILES MODIFIED:
|
|
520
|
+
${files.slice(0, 10).map(f => `- ${f}`).join('\n') || '- None recorded'}
|
|
521
|
+
|
|
522
|
+
Please continue from where you left off.`;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Check if task analysis is available
|
|
526
|
+
*/
|
|
527
|
+
export function isTaskAnalysisAvailable() {
|
|
528
|
+
return !!(process.env.ANTHROPIC_API_KEY || process.env.GROV_API_KEY);
|
|
529
|
+
}
|
|
530
|
+
/**
|
|
531
|
+
* Analyze task context to determine task status
|
|
532
|
+
* Called after each main model response to orchestrate sessions
|
|
533
|
+
* Also compresses reasoning for steps if assistantResponse > 1000 chars
|
|
534
|
+
*/
|
|
535
|
+
export async function analyzeTaskContext(currentSession, latestUserMessage, recentSteps, assistantResponse) {
|
|
536
|
+
const client = getAnthropicClient();
|
|
537
|
+
const stepsText = recentSteps.slice(0, 5).map(s => {
|
|
538
|
+
let desc = `- ${s.action_type}`;
|
|
539
|
+
if (s.files.length > 0) {
|
|
540
|
+
desc += `: ${s.files.slice(0, 3).join(', ')}`;
|
|
541
|
+
}
|
|
542
|
+
return desc;
|
|
543
|
+
}).join('\n') || 'None';
|
|
544
|
+
// Check if we need to compress reasoning
|
|
545
|
+
const needsCompression = assistantResponse.length > 1000;
|
|
546
|
+
const compressionInstruction = needsCompression
|
|
547
|
+
? `\n "step_reasoning": "compressed summary of assistant's actions and reasoning (max 800 chars)"`
|
|
548
|
+
: '';
|
|
549
|
+
const compressionRule = needsCompression
|
|
550
|
+
? '\n- step_reasoning: Summarize what the assistant did and WHY in a concise way (max 800 chars)'
|
|
551
|
+
: '';
|
|
552
|
+
// Extract topic keywords from goal for comparison
|
|
553
|
+
const currentGoalKeywords = currentSession?.original_goal
|
|
554
|
+
? currentSession.original_goal.toLowerCase().match(/\b\w{4,}\b/g)?.slice(0, 10).join(', ') || ''
|
|
555
|
+
: '';
|
|
556
|
+
const prompt = `You are a task orchestrator. Your PRIMARY job is to detect when the user starts a NEW, DIFFERENT task.
|
|
557
|
+
|
|
558
|
+
CURRENT SESSION:
|
|
559
|
+
- Current Goal: "${currentSession?.original_goal || 'No active task'}"
|
|
560
|
+
- Goal Keywords: [${currentGoalKeywords}]
|
|
561
|
+
|
|
562
|
+
LATEST USER MESSAGE:
|
|
563
|
+
"${latestUserMessage.substring(0, 500)}"
|
|
564
|
+
|
|
565
|
+
RECENT ACTIONS (last 5):
|
|
566
|
+
${stepsText}
|
|
567
|
+
|
|
568
|
+
ASSISTANT RESPONSE (truncated):
|
|
569
|
+
"${assistantResponse.substring(0, 1500)}${assistantResponse.length > 1500 ? '...' : ''}"
|
|
570
|
+
|
|
571
|
+
═══════════════════════════════════════════════════════════════
|
|
572
|
+
CRITICAL: Compare the TOPIC of "Current Goal" vs "Latest User Message"
|
|
573
|
+
═══════════════════════════════════════════════════════════════
|
|
574
|
+
|
|
575
|
+
Ask yourself:
|
|
576
|
+
1. Is the user message about the SAME subject/feature/file as the current goal?
|
|
577
|
+
2. Or is it about something COMPLETELY DIFFERENT?
|
|
578
|
+
|
|
579
|
+
EXAMPLES of NEW_TASK (different topic):
|
|
580
|
+
- Goal: "implement authentication" → User: "fix the database migration" → NEW_TASK
|
|
581
|
+
- Goal: "analyze security layer" → User: "create hello.ts script" → NEW_TASK
|
|
582
|
+
- Goal: "refactor user service" → User: "add dark mode to UI" → NEW_TASK
|
|
583
|
+
- Goal: "fix login bug" → User: "write unit tests for payments" → NEW_TASK
|
|
584
|
+
|
|
585
|
+
EXAMPLES of CONTINUE (same topic):
|
|
586
|
+
- Goal: "implement authentication" → User: "now add the logout button" → CONTINUE
|
|
587
|
+
- Goal: "fix login bug" → User: "also check the session timeout" → CONTINUE
|
|
588
|
+
- Goal: "analyze security" → User: "what about rate limiting?" → CONTINUE
|
|
589
|
+
|
|
590
|
+
Return JSON:
|
|
329
591
|
{
|
|
330
|
-
"
|
|
331
|
-
"
|
|
332
|
-
"
|
|
333
|
-
"
|
|
334
|
-
"
|
|
592
|
+
"action": "continue|new_task|subtask|parallel_task|task_complete|subtask_complete",
|
|
593
|
+
"topic_match": "YES if same topic, NO if different topic",
|
|
594
|
+
"task_id": "existing session_id or 'NEW' for new task",
|
|
595
|
+
"current_goal": "the goal based on LATEST user message",
|
|
596
|
+
"reasoning": "1 sentence explaining topic comparison"${compressionInstruction}
|
|
335
597
|
}
|
|
336
598
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
599
|
+
DECISION RULES:
|
|
600
|
+
1. NO current session → "new_task"
|
|
601
|
+
2. topic_match=NO (different subject) → "new_task"
|
|
602
|
+
3. topic_match=YES + user following up → "continue"
|
|
603
|
+
4. Claude said "done/complete/finished" → "task_complete"
|
|
604
|
+
5. Prerequisite work identified → "subtask"${compressionRule}
|
|
605
|
+
|
|
606
|
+
RESPONSE RULES:
|
|
607
|
+
- English only (translate if input is in other language)
|
|
608
|
+
- No emojis
|
|
609
|
+
- Valid JSON only`;
|
|
610
|
+
debugLLM('analyzeTaskContext', `Calling Haiku for task analysis (needsCompression=${needsCompression})`);
|
|
611
|
+
const response = await client.messages.create({
|
|
612
|
+
model: 'claude-haiku-4-5-20251001',
|
|
613
|
+
max_tokens: needsCompression ? 600 : 300,
|
|
614
|
+
messages: [{ role: 'user', content: prompt }],
|
|
340
615
|
});
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
616
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
617
|
+
try {
|
|
618
|
+
// Try to parse JSON from response (may have extra text)
|
|
619
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
620
|
+
if (!jsonMatch) {
|
|
621
|
+
throw new Error('No JSON found in response');
|
|
622
|
+
}
|
|
623
|
+
const analysis = JSON.parse(jsonMatch[0]);
|
|
624
|
+
// If we didn't need compression but have short response, use it directly
|
|
625
|
+
if (!needsCompression && assistantResponse.length > 0) {
|
|
626
|
+
analysis.step_reasoning = assistantResponse.substring(0, 1000);
|
|
627
|
+
}
|
|
628
|
+
debugLLM('analyzeTaskContext', `Result: action=${analysis.action}, topic_match=${analysis.topic_match}, goal=${analysis.current_goal.substring(0, 50)}`);
|
|
629
|
+
return analysis;
|
|
630
|
+
}
|
|
631
|
+
catch (parseError) {
|
|
632
|
+
debugLLM('analyzeTaskContext', `Parse error: ${String(parseError)}, using fallback`);
|
|
633
|
+
// Fallback: continue existing session or create new
|
|
634
|
+
return {
|
|
635
|
+
action: currentSession ? 'continue' : 'new_task',
|
|
636
|
+
task_id: currentSession?.session_id || 'NEW',
|
|
637
|
+
current_goal: latestUserMessage.substring(0, 200),
|
|
638
|
+
reasoning: 'Fallback due to parse error',
|
|
639
|
+
step_reasoning: assistantResponse.substring(0, 1000),
|
|
640
|
+
};
|
|
345
641
|
}
|
|
346
|
-
const parsed = JSON.parse(content.text);
|
|
347
|
-
return {
|
|
348
|
-
goal: parsed.goal || prompt.substring(0, 100),
|
|
349
|
-
expected_scope: parsed.expected_scope || [],
|
|
350
|
-
constraints: parsed.constraints || [],
|
|
351
|
-
success_criteria: parsed.success_criteria || [],
|
|
352
|
-
keywords: parsed.keywords || extractKeywordsBasic(prompt)
|
|
353
|
-
};
|
|
354
642
|
}
|
|
355
643
|
/**
|
|
356
|
-
*
|
|
644
|
+
* Check if reasoning extraction is available
|
|
357
645
|
*/
|
|
358
|
-
function
|
|
359
|
-
return
|
|
360
|
-
goal: prompt.substring(0, 200),
|
|
361
|
-
expected_scope: extractFilesFromPrompt(prompt),
|
|
362
|
-
constraints: [],
|
|
363
|
-
success_criteria: [],
|
|
364
|
-
keywords: extractKeywordsBasic(prompt)
|
|
365
|
-
};
|
|
646
|
+
export function isReasoningExtractionAvailable() {
|
|
647
|
+
return !!process.env.ANTHROPIC_API_KEY || !!process.env.GROV_API_KEY;
|
|
366
648
|
}
|
|
367
649
|
/**
|
|
368
|
-
* Extract
|
|
650
|
+
* Extract reasoning trace and decisions from steps
|
|
651
|
+
* Called at task_complete to populate team memory with rich context
|
|
369
652
|
*/
|
|
370
|
-
function
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const matches = prompt.matchAll(pattern);
|
|
380
|
-
for (const match of matches) {
|
|
381
|
-
const file = match[1].trim();
|
|
382
|
-
if (file && !file.match(/^(http|https|ftp|mailto)/) && !file.match(/^\d+\.\d+/)) {
|
|
383
|
-
files.add(file);
|
|
384
|
-
}
|
|
385
|
-
}
|
|
653
|
+
export async function extractReasoningAndDecisions(stepsReasoning, originalGoal) {
|
|
654
|
+
const client = getAnthropicClient();
|
|
655
|
+
// Combine all steps reasoning into one text
|
|
656
|
+
const combinedReasoning = stepsReasoning
|
|
657
|
+
.filter(r => r && r.length > 10)
|
|
658
|
+
.join('\n\n---\n\n')
|
|
659
|
+
.substring(0, 8000);
|
|
660
|
+
if (combinedReasoning.length < 50) {
|
|
661
|
+
return { reasoning_trace: [], decisions: [] };
|
|
386
662
|
}
|
|
387
|
-
|
|
663
|
+
const prompt = `Analyze Claude's work session and extract structured reasoning and decisions.
|
|
664
|
+
|
|
665
|
+
ORIGINAL GOAL:
|
|
666
|
+
${originalGoal || 'Not specified'}
|
|
667
|
+
|
|
668
|
+
CLAUDE'S WORK (reasoning from each step):
|
|
669
|
+
${combinedReasoning}
|
|
670
|
+
|
|
671
|
+
═══════════════════════════════════════════════════════════════
|
|
672
|
+
EXTRACT TWO THINGS:
|
|
673
|
+
═══════════════════════════════════════════════════════════════
|
|
674
|
+
|
|
675
|
+
1. REASONING TRACE (what was done and WHY):
|
|
676
|
+
- Each entry: "[ACTION] [target] because/to [reason]"
|
|
677
|
+
- Include specific file names, function names when mentioned
|
|
678
|
+
- Focus on WHY decisions were made, not just what
|
|
679
|
+
- Max 10 entries, most important first
|
|
680
|
+
|
|
681
|
+
2. DECISIONS (choices made between alternatives):
|
|
682
|
+
- Only include actual choices/tradeoffs
|
|
683
|
+
- Each must have: what was chosen, why it was chosen
|
|
684
|
+
- Examples: "Chose X over Y because Z"
|
|
685
|
+
- Max 5 decisions
|
|
686
|
+
|
|
687
|
+
Return JSON:
|
|
688
|
+
{
|
|
689
|
+
"reasoning_trace": [
|
|
690
|
+
"Created auth/token-store.ts to separate storage logic from validation",
|
|
691
|
+
"Used Map instead of Object for O(1) lookup performance",
|
|
692
|
+
"Added expiry check in validateToken to prevent stale token usage"
|
|
693
|
+
],
|
|
694
|
+
"decisions": [
|
|
695
|
+
{"choice": "Used SHA256 for token hashing", "reason": "Fast, secure enough for this use case, no external deps"},
|
|
696
|
+
{"choice": "Split into separate files", "reason": "Better testability and single responsibility"}
|
|
697
|
+
]
|
|
388
698
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
699
|
+
|
|
700
|
+
RESPONSE RULES:
|
|
701
|
+
- English only
|
|
702
|
+
- No emojis
|
|
703
|
+
- Valid JSON only
|
|
704
|
+
- Be specific, not vague (bad: "Fixed bug", good: "Fixed null check in validateToken")`;
|
|
705
|
+
debugLLM('extractReasoningAndDecisions', `Analyzing ${stepsReasoning.length} steps, ${combinedReasoning.length} chars`);
|
|
706
|
+
try {
|
|
707
|
+
const response = await client.messages.create({
|
|
708
|
+
model: 'claude-haiku-4-5-20251001',
|
|
709
|
+
max_tokens: 800,
|
|
710
|
+
messages: [{ role: 'user', content: prompt }],
|
|
711
|
+
});
|
|
712
|
+
const text = response.content[0].type === 'text' ? response.content[0].text : '';
|
|
713
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
714
|
+
if (!jsonMatch) {
|
|
715
|
+
debugLLM('extractReasoningAndDecisions', 'No JSON found in response');
|
|
716
|
+
return { reasoning_trace: [], decisions: [] };
|
|
717
|
+
}
|
|
718
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
719
|
+
debugLLM('extractReasoningAndDecisions', `Extracted ${result.reasoning_trace?.length || 0} traces, ${result.decisions?.length || 0} decisions`);
|
|
720
|
+
return {
|
|
721
|
+
reasoning_trace: result.reasoning_trace || [],
|
|
722
|
+
decisions: result.decisions || [],
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
debugLLM('extractReasoningAndDecisions', `Error: ${String(error)}`);
|
|
727
|
+
return { reasoning_trace: [], decisions: [] };
|
|
728
|
+
}
|
|
408
729
|
}
|