grov 0.5.11 → 0.6.13
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/dist/cli/agents/registry.d.ts +17 -0
- package/dist/cli/agents/registry.js +132 -0
- package/dist/cli/commands/agents.d.ts +1 -0
- package/dist/cli/commands/agents.js +48 -0
- package/dist/cli/commands/disable.d.ts +1 -0
- package/dist/cli/commands/disable.js +179 -0
- package/dist/cli/commands/doctor.d.ts +1 -0
- package/dist/cli/commands/doctor.js +157 -0
- package/dist/{commands → cli/commands}/drift-test.js +39 -26
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +90 -0
- package/dist/{commands → cli/commands}/login.js +19 -18
- package/dist/{commands → cli/commands}/logout.js +1 -1
- package/dist/{commands → cli/commands}/proxy-status.js +1 -1
- package/dist/cli/commands/setup.d.ts +6 -0
- package/dist/cli/commands/setup.js +309 -0
- package/dist/{commands → cli/commands}/status.js +1 -1
- package/dist/{commands → cli/commands}/sync.d.ts +1 -0
- package/dist/{commands → cli/commands}/sync.js +59 -4
- package/dist/{commands → cli/commands}/uninstall.js +2 -2
- package/dist/cli/index.js +270 -0
- package/dist/{lib → core/cloud}/cloud-sync.d.ts +3 -3
- package/dist/{lib → core/cloud}/cloud-sync.js +10 -10
- package/dist/{lib → core/extraction}/correction-builder-proxy.d.ts +1 -1
- package/dist/{lib → core/extraction}/correction-builder-proxy.js +0 -4
- package/dist/{lib → core/extraction}/drift-checker-proxy.d.ts +13 -9
- package/dist/core/extraction/drift-checker-proxy.js +510 -0
- package/dist/{lib → core/extraction}/llm-extractor.d.ts +8 -38
- package/dist/{lib → core/extraction}/llm-extractor.js +132 -220
- package/dist/{lib → core}/store/sessions.js +3 -19
- package/dist/core/store/store.d.ts +1 -0
- package/dist/{lib → core/store}/store.js +1 -1
- package/dist/{lib → core}/store/types.d.ts +0 -4
- package/dist/integrations/mcp/cache.d.ts +27 -0
- package/dist/integrations/mcp/cache.js +106 -0
- package/dist/integrations/mcp/capture/antigravity-parser.d.ts +26 -0
- package/dist/integrations/mcp/capture/antigravity-parser.js +272 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.d.ts +24 -0
- package/dist/integrations/mcp/capture/antigravity-scanner.js +153 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.d.ts +29 -0
- package/dist/integrations/mcp/capture/antigravity-sync-tracker.js +115 -0
- package/dist/integrations/mcp/capture/cli-extractor.d.ts +18 -0
- package/dist/integrations/mcp/capture/cli-extractor.js +258 -0
- package/dist/integrations/mcp/capture/cli-synced.d.ts +4 -0
- package/dist/integrations/mcp/capture/cli-synced.js +62 -0
- package/dist/integrations/mcp/capture/cli-transform.d.ts +30 -0
- package/dist/integrations/mcp/capture/cli-transform.js +62 -0
- package/dist/integrations/mcp/capture/cli-watcher.d.ts +31 -0
- package/dist/integrations/mcp/capture/cli-watcher.js +106 -0
- package/dist/integrations/mcp/capture/hook-handler.d.ts +2 -0
- package/dist/integrations/mcp/capture/hook-handler.js +157 -0
- package/dist/integrations/mcp/capture/sqlite-reader.d.ts +35 -0
- package/dist/integrations/mcp/capture/sqlite-reader.js +388 -0
- package/dist/integrations/mcp/capture/sync-tracker.d.ts +16 -0
- package/dist/integrations/mcp/capture/sync-tracker.js +102 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.d.ts +19 -0
- package/dist/integrations/mcp/clients/cursor/rules-installer.js +123 -0
- package/dist/integrations/mcp/index.d.ts +1 -0
- package/dist/integrations/mcp/index.js +94 -0
- package/dist/integrations/mcp/logger.d.ts +8 -0
- package/dist/integrations/mcp/logger.js +50 -0
- package/dist/integrations/mcp/server.d.ts +5 -0
- package/dist/integrations/mcp/server.js +58 -0
- package/dist/integrations/mcp/tools/expand.d.ts +1 -0
- package/dist/integrations/mcp/tools/expand.js +53 -0
- package/dist/integrations/mcp/tools/preview.d.ts +1 -0
- package/dist/integrations/mcp/tools/preview.js +64 -0
- package/dist/integrations/proxy/agents/base.d.ts +43 -0
- package/dist/integrations/proxy/agents/base.js +13 -0
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.d.ts +4 -8
- package/dist/{proxy/utils → integrations/proxy/agents/claude}/extractors.js +4 -33
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.d.ts +1 -1
- package/dist/{proxy → integrations/proxy/agents/claude}/forwarder.js +22 -6
- package/dist/integrations/proxy/agents/claude/index.d.ts +43 -0
- package/dist/integrations/proxy/agents/claude/index.js +386 -0
- package/dist/{proxy/action-parser.d.ts → integrations/proxy/agents/claude/parser.d.ts} +1 -1
- package/dist/integrations/proxy/agents/codex/extractors.d.ts +6 -0
- package/dist/integrations/proxy/agents/codex/extractors.js +49 -0
- package/dist/integrations/proxy/agents/codex/forwarder.d.ts +9 -0
- package/dist/integrations/proxy/agents/codex/forwarder.js +125 -0
- package/dist/integrations/proxy/agents/codex/index.d.ts +44 -0
- package/dist/integrations/proxy/agents/codex/index.js +371 -0
- package/dist/integrations/proxy/agents/codex/parser.d.ts +11 -0
- package/dist/integrations/proxy/agents/codex/parser.js +104 -0
- package/dist/integrations/proxy/agents/codex/patch.d.ts +12 -0
- package/dist/integrations/proxy/agents/codex/patch.js +40 -0
- package/dist/integrations/proxy/agents/codex/settings.d.ts +18 -0
- package/dist/integrations/proxy/agents/codex/settings.js +73 -0
- package/dist/integrations/proxy/agents/codex/types.d.ts +59 -0
- package/dist/integrations/proxy/agents/codex/types.js +2 -0
- package/dist/integrations/proxy/agents/index.d.ts +11 -0
- package/dist/integrations/proxy/agents/index.js +25 -0
- package/dist/integrations/proxy/agents/types.d.ts +77 -0
- package/dist/integrations/proxy/agents/types.js +2 -0
- package/dist/{proxy → integrations/proxy/cache}/extended-cache.js +2 -6
- package/dist/{proxy → integrations/proxy}/config.js +1 -1
- package/dist/{proxy → integrations/proxy}/handlers/preprocess.d.ts +3 -3
- package/dist/integrations/proxy/handlers/preprocess.js +194 -0
- package/dist/integrations/proxy/index.js +20 -0
- package/dist/integrations/proxy/injection/memory-injection.d.ts +56 -0
- package/dist/integrations/proxy/injection/memory-injection.js +252 -0
- package/dist/integrations/proxy/orchestrator.d.ts +30 -0
- package/dist/integrations/proxy/orchestrator.js +954 -0
- package/dist/integrations/proxy/request-processor.d.ts +14 -0
- package/dist/integrations/proxy/request-processor.js +68 -0
- package/dist/{proxy → integrations/proxy}/response-processor.d.ts +4 -3
- package/dist/{proxy → integrations/proxy}/response-processor.js +51 -43
- package/dist/{proxy → integrations/proxy}/server.d.ts +0 -1
- package/dist/integrations/proxy/server.js +146 -0
- package/dist/{proxy → integrations/proxy}/types.d.ts +4 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.d.ts +1 -0
- package/dist/{proxy → integrations/proxy}/utils/logging.js +5 -0
- package/package.json +31 -10
- package/postinstall.js +62 -6
- package/dist/cli.js +0 -149
- package/dist/commands/capture.d.ts +0 -6
- package/dist/commands/capture.js +0 -324
- package/dist/commands/disable.d.ts +0 -1
- package/dist/commands/disable.js +0 -14
- package/dist/commands/doctor.d.ts +0 -1
- package/dist/commands/doctor.js +0 -89
- package/dist/commands/init.d.ts +0 -1
- package/dist/commands/init.js +0 -52
- package/dist/commands/inject.d.ts +0 -5
- package/dist/commands/inject.js +0 -88
- package/dist/commands/prompt-inject.d.ts +0 -4
- package/dist/commands/prompt-inject.js +0 -451
- package/dist/commands/unregister.d.ts +0 -1
- package/dist/commands/unregister.js +0 -28
- package/dist/lib/anchor-extractor.d.ts +0 -30
- package/dist/lib/anchor-extractor.js +0 -296
- package/dist/lib/correction-builder.d.ts +0 -10
- package/dist/lib/correction-builder.js +0 -226
- package/dist/lib/drift-checker-proxy.js +0 -373
- package/dist/lib/drift-checker.d.ts +0 -66
- package/dist/lib/drift-checker.js +0 -341
- package/dist/lib/hooks.d.ts +0 -38
- package/dist/lib/hooks.js +0 -291
- package/dist/lib/jsonl-parser.d.ts +0 -87
- package/dist/lib/jsonl-parser.js +0 -281
- package/dist/lib/session-parser.d.ts +0 -44
- package/dist/lib/session-parser.js +0 -256
- package/dist/lib/store.d.ts +0 -1
- package/dist/proxy/cache.d.ts +0 -32
- package/dist/proxy/cache.js +0 -47
- package/dist/proxy/handlers/preprocess.js +0 -186
- package/dist/proxy/index.js +0 -30
- package/dist/proxy/injection/delta-tracking.d.ts +0 -11
- package/dist/proxy/injection/delta-tracking.js +0 -94
- package/dist/proxy/injection/injectors.d.ts +0 -7
- package/dist/proxy/injection/injectors.js +0 -139
- package/dist/proxy/request-processor.d.ts +0 -27
- package/dist/proxy/request-processor.js +0 -233
- package/dist/proxy/server.js +0 -1289
- /package/dist/{commands → cli/commands}/drift-test.d.ts +0 -0
- /package/dist/{commands → cli/commands}/login.d.ts +0 -0
- /package/dist/{commands → cli/commands}/logout.d.ts +0 -0
- /package/dist/{commands → cli/commands}/proxy-status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/status.d.ts +0 -0
- /package/dist/{commands → cli/commands}/uninstall.d.ts +0 -0
- /package/dist/{cli.d.ts → cli/index.d.ts} +0 -0
- /package/dist/{lib → core/cloud}/api-client.d.ts +0 -0
- /package/dist/{lib → core/cloud}/api-client.js +0 -0
- /package/dist/{lib → core/cloud}/credentials.d.ts +0 -0
- /package/dist/{lib → core/cloud}/credentials.js +0 -0
- /package/dist/{lib → core}/store/convenience.d.ts +0 -0
- /package/dist/{lib → core}/store/convenience.js +0 -0
- /package/dist/{lib → core}/store/database.d.ts +0 -0
- /package/dist/{lib → core}/store/database.js +0 -0
- /package/dist/{lib → core}/store/drift.d.ts +0 -0
- /package/dist/{lib → core}/store/drift.js +0 -0
- /package/dist/{lib → core}/store/index.d.ts +0 -0
- /package/dist/{lib → core}/store/index.js +0 -0
- /package/dist/{lib → core}/store/sessions.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.d.ts +0 -0
- /package/dist/{lib → core}/store/steps.js +0 -0
- /package/dist/{lib → core}/store/tasks.d.ts +0 -0
- /package/dist/{lib → core}/store/tasks.js +0 -0
- /package/dist/{lib → core}/store/types.js +0 -0
- /package/dist/{proxy/action-parser.js → integrations/proxy/agents/claude/parser.js} +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.d.ts +0 -0
- /package/dist/{lib → integrations/proxy/agents/claude}/settings.js +0 -0
- /package/dist/{proxy → integrations/proxy/cache}/extended-cache.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/config.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/index.d.ts +0 -0
- /package/dist/{proxy → integrations/proxy}/types.js +0 -0
- /package/dist/{lib → utils}/debug.d.ts +0 -0
- /package/dist/{lib → utils}/debug.js +0 -0
- /package/dist/{lib → utils}/utils.d.ts +0 -0
- /package/dist/{lib → utils}/utils.js +0 -0
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
// Grov Proxy Orchestrator - Shared business logic for all agents
|
|
2
|
+
// Handles session management, task orchestration, drift detection, and memory injection
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { config, buildSafeHeaders } from './config.js';
|
|
5
|
+
import { extendedCache, evictOldestCacheEntry } from './cache/extended-cache.js';
|
|
6
|
+
import { getNextRequestId, taskLog, proxyLog, logTokenUsage } from './utils/logging.js';
|
|
7
|
+
import { preProcessRequest, setPendingPlanClear } from './handlers/preprocess.js';
|
|
8
|
+
import { createSessionState, getSessionState, updateSessionState, createStep, updateTokenCount, logDriftEvent, getRecentSteps, getValidatedSteps, updateSessionMode, markWaitingForRecovery, incrementEscalation, updateLastChecked, getActiveSessionForUser, deleteSessionState, deleteStepsForSession, updateRecentStepsReasoning, markSessionCompleted, getCompletedSessionForProject, } from '../../core/store/store.js';
|
|
9
|
+
import { checkDrift, scoreToCorrectionLevel, shouldSkipSteps, checkRecoveryAlignment, } from '../../core/extraction/drift-checker-proxy.js';
|
|
10
|
+
import { buildCorrection, formatCorrectionForInjection } from '../../core/extraction/correction-builder-proxy.js';
|
|
11
|
+
import { generateSessionSummary, analyzeTaskContext, } from '../../core/extraction/llm-extractor.js';
|
|
12
|
+
import { saveToTeamMemory } from './response-processor.js';
|
|
13
|
+
import { getCachedMemoryById, buildExpandedMemory, addInjectionRecord, hasToolCycleAtPosition, } from './injection/memory-injection.js';
|
|
14
|
+
// In-memory state
|
|
15
|
+
const lastDriftResults = new Map();
|
|
16
|
+
const lastMessageCount = new Map();
|
|
17
|
+
const activeSessions = new Map();
|
|
18
|
+
/**
|
|
19
|
+
* Main entry point for handling agent requests
|
|
20
|
+
*/
|
|
21
|
+
export async function handleAgentRequest(context) {
|
|
22
|
+
const { adapter, body, headers, rawBody, logger } = context;
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
// Extract model from body
|
|
25
|
+
const requestBody = body;
|
|
26
|
+
const model = requestBody.model || '';
|
|
27
|
+
// Skip subagent models (Haiku for Claude, mini for Codex)
|
|
28
|
+
if (adapter.isSubagentModel(model)) {
|
|
29
|
+
logger.info({ msg: 'Skipping subagent', model });
|
|
30
|
+
try {
|
|
31
|
+
const result = await adapter.forward({ ...requestBody, stream: false }, headers, rawBody);
|
|
32
|
+
return {
|
|
33
|
+
statusCode: result.statusCode,
|
|
34
|
+
contentType: 'application/json',
|
|
35
|
+
headers: adapter.filterResponseHeaders(result.headers),
|
|
36
|
+
body: JSON.stringify(result.body),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
logger.error?.({ msg: 'Subagent forward error', error: String(error) });
|
|
41
|
+
return {
|
|
42
|
+
statusCode: 502,
|
|
43
|
+
contentType: 'application/json',
|
|
44
|
+
headers: {},
|
|
45
|
+
body: JSON.stringify({ error: { type: 'proxy_error', message: 'Bad gateway' } }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Get or create session
|
|
50
|
+
const sessionInfo = await getOrCreateSession(adapter, body, logger);
|
|
51
|
+
sessionInfo.promptCount++;
|
|
52
|
+
activeSessions.set(sessionInfo.sessionId, {
|
|
53
|
+
sessionId: sessionInfo.sessionId,
|
|
54
|
+
promptCount: sessionInfo.promptCount,
|
|
55
|
+
projectPath: sessionInfo.projectPath,
|
|
56
|
+
});
|
|
57
|
+
const currentRequestId = getNextRequestId();
|
|
58
|
+
logger.info({
|
|
59
|
+
msg: 'Incoming request',
|
|
60
|
+
agent: adapter.name,
|
|
61
|
+
sessionId: sessionInfo.sessionId.substring(0, 8),
|
|
62
|
+
promptCount: sessionInfo.promptCount,
|
|
63
|
+
model,
|
|
64
|
+
messageCount: getMessageCount(body),
|
|
65
|
+
});
|
|
66
|
+
proxyLog({
|
|
67
|
+
requestId: currentRequestId,
|
|
68
|
+
type: 'REQUEST',
|
|
69
|
+
sessionId: sessionInfo.sessionId.substring(0, 8),
|
|
70
|
+
data: {
|
|
71
|
+
agent: adapter.name,
|
|
72
|
+
model,
|
|
73
|
+
messageCount: getMessageCount(body),
|
|
74
|
+
promptCount: sessionInfo.promptCount,
|
|
75
|
+
rawBodySize: rawBody?.length || 0,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
// Pre-process request for memory injection (agent-agnostic)
|
|
79
|
+
const processedBody = await preProcessRequest(adapter, requestBody, sessionInfo, logger, detectRequestType);
|
|
80
|
+
const systemInjection = processedBody.__grovInjection;
|
|
81
|
+
const userMsgInjection = processedBody.__grovUserMsgInjection;
|
|
82
|
+
const rawUserPrompt = processedBody.__grovRawUserPrompt;
|
|
83
|
+
// Build final body with injections using adapter methods
|
|
84
|
+
let rawBodyStr = rawBody?.toString('utf-8') || '';
|
|
85
|
+
let systemInjectionSize = 0;
|
|
86
|
+
let userMsgInjectionSize = 0;
|
|
87
|
+
let systemSuccess = false;
|
|
88
|
+
let userMsgSuccess = false;
|
|
89
|
+
if (systemInjection && rawBodyStr) {
|
|
90
|
+
const result = adapter.injectIntoRawSystemPrompt(rawBodyStr, '\n\n' + systemInjection);
|
|
91
|
+
rawBodyStr = result.modified;
|
|
92
|
+
systemInjectionSize = systemInjection.length;
|
|
93
|
+
systemSuccess = result.success;
|
|
94
|
+
}
|
|
95
|
+
if (userMsgInjection && rawBodyStr) {
|
|
96
|
+
const beforeLen = rawBodyStr.length;
|
|
97
|
+
rawBodyStr = adapter.injectIntoRawUserMessage(rawBodyStr, userMsgInjection);
|
|
98
|
+
const afterLen = rawBodyStr.length;
|
|
99
|
+
userMsgInjectionSize = userMsgInjection.length;
|
|
100
|
+
userMsgSuccess = afterLen > beforeLen;
|
|
101
|
+
}
|
|
102
|
+
// Inject grov_expand tool if needed
|
|
103
|
+
const hasGrovExpandInProcessed = processedBody.tools &&
|
|
104
|
+
Array.isArray(processedBody.tools) &&
|
|
105
|
+
processedBody.tools.some(t => t.name === 'grov_expand');
|
|
106
|
+
if (hasGrovExpandInProcessed && rawBodyStr) {
|
|
107
|
+
const toolDef = adapter.buildGrovExpandTool();
|
|
108
|
+
const result = adapter.injectToolIntoRawBody(rawBodyStr, toolDef);
|
|
109
|
+
if (result.success) {
|
|
110
|
+
rawBodyStr = result.modified;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Determine final body
|
|
114
|
+
let finalBodyToSend;
|
|
115
|
+
const reconstructedCount = processedBody.__grovReconstructedCount || 0;
|
|
116
|
+
if (systemInjection || userMsgInjection) {
|
|
117
|
+
finalBodyToSend = Buffer.from(rawBodyStr, 'utf-8');
|
|
118
|
+
const wasCached = processedBody.__grovInjectionCached;
|
|
119
|
+
proxyLog({
|
|
120
|
+
requestId: currentRequestId,
|
|
121
|
+
type: 'INJECTION',
|
|
122
|
+
sessionId: sessionInfo.sessionId.substring(0, 8),
|
|
123
|
+
data: {
|
|
124
|
+
systemInjectionSize,
|
|
125
|
+
userMsgInjectionSize,
|
|
126
|
+
totalInjectionSize: systemInjectionSize + userMsgInjectionSize,
|
|
127
|
+
originalSize: rawBody?.length || 0,
|
|
128
|
+
finalSize: rawBodyStr.length,
|
|
129
|
+
systemSuccess,
|
|
130
|
+
userMsgSuccess,
|
|
131
|
+
teamMemoryCached: wasCached,
|
|
132
|
+
systemInjectionPreview: systemInjection ? systemInjection.substring(0, 200) + (systemInjection.length > 200 ? '...' : '') : null,
|
|
133
|
+
userMsgInjectionContent: userMsgInjection || null,
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
else if (reconstructedCount > 0) {
|
|
138
|
+
const { __grovInjection, __grovUserMsgInjection, __grovInjectionCached, __grovReconstructedCount, __grovOriginalLastUserPos, __grovRawUserPrompt: _rawPrompt1, ...cleanBody } = processedBody;
|
|
139
|
+
finalBodyToSend = Buffer.from(JSON.stringify(cleanBody), 'utf-8');
|
|
140
|
+
}
|
|
141
|
+
else if (rawBody) {
|
|
142
|
+
finalBodyToSend = rawBody;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
finalBodyToSend = Buffer.from(JSON.stringify(processedBody), 'utf-8');
|
|
146
|
+
}
|
|
147
|
+
const forwardStart = Date.now();
|
|
148
|
+
try {
|
|
149
|
+
// Forward to upstream API
|
|
150
|
+
let result = await adapter.forward(processedBody, headers, finalBodyToSend);
|
|
151
|
+
// Handle grov_expand internal tool loop
|
|
152
|
+
let loopCount = 0;
|
|
153
|
+
const maxLoops = 5;
|
|
154
|
+
const { __grovInjection: _i, __grovUserMsgInjection: _u, __grovInjectionCached: _c, __grovReconstructedCount: _r, __grovOriginalLastUserPos, __grovRawUserPrompt: _p, ...cleanProcessedBody } = processedBody;
|
|
155
|
+
let accumulatedBody = cleanProcessedBody;
|
|
156
|
+
while (result.statusCode === 200 &&
|
|
157
|
+
adapter.isValidResponse(result.body) &&
|
|
158
|
+
adapter.isToolUse(result.body) &&
|
|
159
|
+
loopCount < maxLoops) {
|
|
160
|
+
const grovExpandBlock = adapter.findInternalToolUse(result.body, 'grov_expand');
|
|
161
|
+
if (!grovExpandBlock)
|
|
162
|
+
break;
|
|
163
|
+
loopCount++;
|
|
164
|
+
const allToolUseBlocks = adapter.getToolUseBlocks(result.body);
|
|
165
|
+
const toolResultBlocks = [];
|
|
166
|
+
let grovExpandResult = '';
|
|
167
|
+
for (const block of allToolUseBlocks) {
|
|
168
|
+
if (block.name === 'grov_expand') {
|
|
169
|
+
const ids = block.input?.ids || [];
|
|
170
|
+
const expandedParts = [];
|
|
171
|
+
for (const id of ids) {
|
|
172
|
+
const memory = getCachedMemoryById(sessionInfo.projectPath, id);
|
|
173
|
+
if (memory) {
|
|
174
|
+
expandedParts.push(buildExpandedMemory(memory));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
expandedParts.push(`Memory #${id} not found - it may be from an older conversation. Only expand IDs from the CURRENT knowledge base.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (ids.length > 0) {
|
|
181
|
+
const expandedCount = expandedParts.filter(p => !p.includes('not found')).length;
|
|
182
|
+
console.log(`[MEMORY] Expanded ${expandedCount}/${ids.length} memories`);
|
|
183
|
+
}
|
|
184
|
+
grovExpandResult = expandedParts.join('\n\n');
|
|
185
|
+
toolResultBlocks.push({
|
|
186
|
+
type: 'tool_result',
|
|
187
|
+
tool_use_id: block.id,
|
|
188
|
+
content: grovExpandResult,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
toolResultBlocks.push({
|
|
193
|
+
type: 'tool_result',
|
|
194
|
+
tool_use_id: block.id,
|
|
195
|
+
content: 'This tool call was deferred. Use the expanded project knowledge base context above, then call this tool separately if still needed.',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const pos = __grovOriginalLastUserPos ?? 0;
|
|
200
|
+
if (!hasToolCycleAtPosition(sessionInfo.projectPath, pos)) {
|
|
201
|
+
addInjectionRecord(sessionInfo.projectPath, {
|
|
202
|
+
position: pos,
|
|
203
|
+
type: 'tool_cycle',
|
|
204
|
+
toolUse: { id: grovExpandBlock.id, name: 'grov_expand', input: grovExpandBlock.input },
|
|
205
|
+
toolResult: grovExpandResult,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
const assistantContent = getAssistantContent(result.body, adapter);
|
|
209
|
+
const messages = [...accumulatedBody.messages];
|
|
210
|
+
messages.push({ role: 'assistant', content: assistantContent });
|
|
211
|
+
messages.push({ role: 'user', content: toolResultBlocks });
|
|
212
|
+
accumulatedBody = { ...accumulatedBody, messages };
|
|
213
|
+
result = await adapter.forward(accumulatedBody, headers);
|
|
214
|
+
}
|
|
215
|
+
const forwardLatency = Date.now() - forwardStart;
|
|
216
|
+
// Fire-and-forget post-processing
|
|
217
|
+
if (result.statusCode === 200 && adapter.isValidResponse(result.body)) {
|
|
218
|
+
const extendedCacheData = config.EXTENDED_CACHE_ENABLED ? {
|
|
219
|
+
headers: buildSafeHeaders(headers),
|
|
220
|
+
rawBody: finalBodyToSend,
|
|
221
|
+
} : undefined;
|
|
222
|
+
postProcessResponse(adapter, result.body, sessionInfo, processedBody, logger, extendedCacheData, headers).catch(err => console.error('[GROV] postProcess error:', err));
|
|
223
|
+
}
|
|
224
|
+
const latency = Date.now() - startTime;
|
|
225
|
+
const filteredHeaders = adapter.filterResponseHeaders(result.headers);
|
|
226
|
+
if (adapter.isValidResponse(result.body)) {
|
|
227
|
+
const usage = adapter.extractUsage(result.body);
|
|
228
|
+
logTokenUsage(currentRequestId, usage, latency);
|
|
229
|
+
proxyLog({
|
|
230
|
+
requestId: currentRequestId,
|
|
231
|
+
type: 'RESPONSE',
|
|
232
|
+
sessionId: sessionInfo.sessionId.substring(0, 8),
|
|
233
|
+
data: {
|
|
234
|
+
statusCode: result.statusCode,
|
|
235
|
+
latencyMs: latency,
|
|
236
|
+
forwardLatencyMs: forwardLatency,
|
|
237
|
+
inputTokens: usage.inputTokens,
|
|
238
|
+
outputTokens: usage.outputTokens,
|
|
239
|
+
cacheCreation: usage.cacheCreation,
|
|
240
|
+
cacheRead: usage.cacheRead,
|
|
241
|
+
cacheHitRatio: usage.cacheRead > 0 ? (usage.cacheRead / (usage.cacheRead + usage.cacheCreation)).toFixed(2) : '0.00',
|
|
242
|
+
wasSSE: result.wasSSE,
|
|
243
|
+
grovExpandLoops: loopCount > 0 ? loopCount : undefined,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
logger.info({
|
|
248
|
+
msg: 'Request complete',
|
|
249
|
+
statusCode: result.statusCode,
|
|
250
|
+
latencyMs: latency,
|
|
251
|
+
wasSSE: result.wasSSE,
|
|
252
|
+
grovExpandLoops: loopCount > 0 ? loopCount : undefined,
|
|
253
|
+
});
|
|
254
|
+
return {
|
|
255
|
+
statusCode: result.statusCode,
|
|
256
|
+
contentType: adapter.getResponseContentType(result.wasSSE || false),
|
|
257
|
+
headers: filteredHeaders,
|
|
258
|
+
body: result.wasSSE ? result.rawBody : JSON.stringify(result.body),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
const err = error;
|
|
263
|
+
if (err.type === 'timeout' || err.type === 'network') {
|
|
264
|
+
logger.error?.({
|
|
265
|
+
msg: 'Forward error',
|
|
266
|
+
type: err.type,
|
|
267
|
+
message: err.message,
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
statusCode: err.statusCode || 502,
|
|
271
|
+
contentType: 'application/json',
|
|
272
|
+
headers: {},
|
|
273
|
+
body: JSON.stringify({
|
|
274
|
+
error: {
|
|
275
|
+
type: 'proxy_error',
|
|
276
|
+
message: err.type === 'timeout' ? 'Gateway timeout' : 'Bad gateway',
|
|
277
|
+
},
|
|
278
|
+
}),
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
logger.error?.({
|
|
282
|
+
msg: 'Unexpected error',
|
|
283
|
+
error: String(error),
|
|
284
|
+
});
|
|
285
|
+
return {
|
|
286
|
+
statusCode: 500,
|
|
287
|
+
contentType: 'application/json',
|
|
288
|
+
headers: {},
|
|
289
|
+
body: JSON.stringify({
|
|
290
|
+
error: {
|
|
291
|
+
type: 'internal_error',
|
|
292
|
+
message: 'Internal proxy error',
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Get or create session for this request
|
|
300
|
+
*/
|
|
301
|
+
async function getOrCreateSession(adapter, body, logger) {
|
|
302
|
+
const projectPath = adapter.extractProjectPath(body) || process.cwd();
|
|
303
|
+
const existingSession = getActiveSessionForUser(projectPath);
|
|
304
|
+
if (existingSession) {
|
|
305
|
+
let sessionInfo = activeSessions.get(existingSession.session_id);
|
|
306
|
+
if (!sessionInfo) {
|
|
307
|
+
sessionInfo = {
|
|
308
|
+
sessionId: existingSession.session_id,
|
|
309
|
+
promptCount: 0,
|
|
310
|
+
projectPath,
|
|
311
|
+
};
|
|
312
|
+
activeSessions.set(existingSession.session_id, sessionInfo);
|
|
313
|
+
}
|
|
314
|
+
logger.info({
|
|
315
|
+
msg: 'Found existing session',
|
|
316
|
+
sessionId: existingSession.session_id.substring(0, 8),
|
|
317
|
+
goal: existingSession.original_goal?.substring(0, 50),
|
|
318
|
+
});
|
|
319
|
+
return { ...sessionInfo, isNew: false, currentSession: existingSession, completedSession: null };
|
|
320
|
+
}
|
|
321
|
+
const completedSession = getCompletedSessionForProject(projectPath);
|
|
322
|
+
if (completedSession) {
|
|
323
|
+
logger.info({
|
|
324
|
+
msg: 'Found recently completed session for comparison',
|
|
325
|
+
sessionId: completedSession.session_id.substring(0, 8),
|
|
326
|
+
goal: completedSession.original_goal?.substring(0, 50),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
const tempSessionId = randomUUID();
|
|
330
|
+
const sessionInfo = {
|
|
331
|
+
sessionId: tempSessionId,
|
|
332
|
+
promptCount: 0,
|
|
333
|
+
projectPath,
|
|
334
|
+
};
|
|
335
|
+
activeSessions.set(tempSessionId, sessionInfo);
|
|
336
|
+
logger.info({ msg: 'No existing session, will create after task analysis' });
|
|
337
|
+
return { ...sessionInfo, isNew: true, currentSession: null, completedSession };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Detect request type: first, continuation, or retry
|
|
341
|
+
*/
|
|
342
|
+
function detectRequestType(messages, projectPath) {
|
|
343
|
+
const currentCount = messages?.length || 0;
|
|
344
|
+
const lastCount = lastMessageCount.get(projectPath);
|
|
345
|
+
lastMessageCount.set(projectPath, currentCount);
|
|
346
|
+
if (lastCount !== undefined && currentCount === lastCount) {
|
|
347
|
+
return 'retry';
|
|
348
|
+
}
|
|
349
|
+
if (!messages || messages.length === 0)
|
|
350
|
+
return 'first';
|
|
351
|
+
const lastMessage = messages[messages.length - 1];
|
|
352
|
+
if (lastMessage.role === 'user') {
|
|
353
|
+
const content = lastMessage.content;
|
|
354
|
+
if (Array.isArray(content)) {
|
|
355
|
+
const hasToolResult = content.some((block) => typeof block === 'object' && block !== null && block.type === 'tool_result');
|
|
356
|
+
if (hasToolResult)
|
|
357
|
+
return 'continuation';
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return 'first';
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Post-process response - task orchestration, drift detection, step recording
|
|
364
|
+
*/
|
|
365
|
+
async function postProcessResponse(adapter, response, sessionInfo, requestBody, logger, extendedCacheData, requestHeaders) {
|
|
366
|
+
const actions = adapter.parseActions(response);
|
|
367
|
+
const textContent = adapter.extractTextContent(response);
|
|
368
|
+
const messages = getMessages(requestBody);
|
|
369
|
+
const latestUserMessage = adapter.extractGoal(messages) || '';
|
|
370
|
+
const rawUserPrompt = requestBody.__grovRawUserPrompt;
|
|
371
|
+
const recentSteps = sessionInfo.currentSession
|
|
372
|
+
? getRecentSteps(sessionInfo.currentSession.session_id, 5)
|
|
373
|
+
: [];
|
|
374
|
+
let activeSessionId = sessionInfo.sessionId;
|
|
375
|
+
let activeSession = sessionInfo.currentSession;
|
|
376
|
+
const isEndTurn = adapter.isEndTurn(response);
|
|
377
|
+
const isWarmup = latestUserMessage.toLowerCase().trim() === 'warmup';
|
|
378
|
+
if (isWarmup) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// Extended cache capture on end_turn
|
|
382
|
+
if (isEndTurn && extendedCacheData) {
|
|
383
|
+
const cacheKey = sessionInfo.projectPath;
|
|
384
|
+
if (!extendedCache.has(cacheKey)) {
|
|
385
|
+
evictOldestCacheEntry();
|
|
386
|
+
}
|
|
387
|
+
extendedCache.set(cacheKey, {
|
|
388
|
+
headers: extendedCacheData.headers,
|
|
389
|
+
rawBody: extendedCacheData.rawBody,
|
|
390
|
+
timestamp: Date.now(),
|
|
391
|
+
keepAliveCount: 0,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
// Skip task orchestration if not end_turn
|
|
395
|
+
if (!isEndTurn) {
|
|
396
|
+
if (sessionInfo.currentSession) {
|
|
397
|
+
activeSessionId = sessionInfo.currentSession.session_id;
|
|
398
|
+
activeSession = sessionInfo.currentSession;
|
|
399
|
+
}
|
|
400
|
+
else if (!activeSession) {
|
|
401
|
+
const newSessionId = randomUUID();
|
|
402
|
+
activeSession = createSessionState({
|
|
403
|
+
session_id: newSessionId,
|
|
404
|
+
project_path: sessionInfo.projectPath,
|
|
405
|
+
original_goal: '',
|
|
406
|
+
raw_user_prompt: rawUserPrompt,
|
|
407
|
+
task_type: 'main',
|
|
408
|
+
});
|
|
409
|
+
activeSessionId = newSessionId;
|
|
410
|
+
activeSessions.set(newSessionId, {
|
|
411
|
+
sessionId: newSessionId,
|
|
412
|
+
promptCount: 1,
|
|
413
|
+
projectPath: sessionInfo.projectPath,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
const sessionForComparison = sessionInfo.currentSession || sessionInfo.completedSession;
|
|
419
|
+
const conversationHistory = adapter.extractHistory(messages);
|
|
420
|
+
try {
|
|
421
|
+
const taskAnalysis = await analyzeTaskContext(sessionForComparison, latestUserMessage, recentSteps, textContent, conversationHistory, requestHeaders);
|
|
422
|
+
logger.info({
|
|
423
|
+
msg: 'Task analysis',
|
|
424
|
+
action: taskAnalysis.action,
|
|
425
|
+
task_type: taskAnalysis.task_type,
|
|
426
|
+
goal: taskAnalysis.current_goal?.substring(0, 50),
|
|
427
|
+
reasoning: taskAnalysis.reasoning,
|
|
428
|
+
});
|
|
429
|
+
taskLog('TASK_ANALYSIS', {
|
|
430
|
+
sessionId: sessionInfo.sessionId,
|
|
431
|
+
action: taskAnalysis.action,
|
|
432
|
+
task_type: taskAnalysis.task_type,
|
|
433
|
+
goal: taskAnalysis.current_goal || '',
|
|
434
|
+
reasoning: taskAnalysis.reasoning || '',
|
|
435
|
+
userMessage: latestUserMessage.substring(0, 80),
|
|
436
|
+
hasCurrentSession: !!sessionInfo.currentSession,
|
|
437
|
+
hasCompletedSession: !!sessionInfo.completedSession,
|
|
438
|
+
});
|
|
439
|
+
if (taskAnalysis.step_reasoning && activeSessionId) {
|
|
440
|
+
const updatedCount = updateRecentStepsReasoning(activeSessionId, taskAnalysis.step_reasoning);
|
|
441
|
+
taskLog('STEP_REASONING', {
|
|
442
|
+
sessionId: activeSessionId,
|
|
443
|
+
stepsUpdated: updatedCount,
|
|
444
|
+
reasoningEntries: Object.keys(taskAnalysis.step_reasoning).length,
|
|
445
|
+
stepIds: Object.keys(taskAnalysis.step_reasoning).join(','),
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
// Task orchestration switch
|
|
449
|
+
switch (taskAnalysis.action) {
|
|
450
|
+
case 'continue':
|
|
451
|
+
if (sessionInfo.currentSession) {
|
|
452
|
+
activeSessionId = sessionInfo.currentSession.session_id;
|
|
453
|
+
activeSession = sessionInfo.currentSession;
|
|
454
|
+
if (taskAnalysis.current_goal &&
|
|
455
|
+
taskAnalysis.current_goal !== activeSession.original_goal &&
|
|
456
|
+
latestUserMessage.length > 30) {
|
|
457
|
+
updateSessionState(activeSessionId, {
|
|
458
|
+
original_goal: taskAnalysis.current_goal,
|
|
459
|
+
});
|
|
460
|
+
activeSession.original_goal = taskAnalysis.current_goal;
|
|
461
|
+
}
|
|
462
|
+
taskLog('ORCHESTRATION_CONTINUE', {
|
|
463
|
+
sessionId: activeSessionId,
|
|
464
|
+
source: 'current_session',
|
|
465
|
+
goal: activeSession.original_goal,
|
|
466
|
+
goalUpdated: taskAnalysis.current_goal !== activeSession.original_goal,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
else if (sessionInfo.completedSession) {
|
|
470
|
+
activeSessionId = sessionInfo.completedSession.session_id;
|
|
471
|
+
activeSession = sessionInfo.completedSession;
|
|
472
|
+
updateSessionState(activeSessionId, {
|
|
473
|
+
status: 'active',
|
|
474
|
+
original_goal: taskAnalysis.current_goal || activeSession.original_goal,
|
|
475
|
+
});
|
|
476
|
+
activeSession.status = 'active';
|
|
477
|
+
activeSessions.set(activeSessionId, {
|
|
478
|
+
sessionId: activeSessionId,
|
|
479
|
+
promptCount: 1,
|
|
480
|
+
projectPath: sessionInfo.projectPath,
|
|
481
|
+
});
|
|
482
|
+
taskLog('ORCHESTRATION_CONTINUE', {
|
|
483
|
+
sessionId: activeSessionId,
|
|
484
|
+
source: 'reactivated_completed',
|
|
485
|
+
goal: activeSession.original_goal,
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
case 'new_task': {
|
|
490
|
+
if (sessionInfo.completedSession) {
|
|
491
|
+
deleteStepsForSession(sessionInfo.completedSession.session_id);
|
|
492
|
+
deleteSessionState(sessionInfo.completedSession.session_id);
|
|
493
|
+
}
|
|
494
|
+
const newSessionId = randomUUID();
|
|
495
|
+
activeSession = createSessionState({
|
|
496
|
+
session_id: newSessionId,
|
|
497
|
+
project_path: sessionInfo.projectPath,
|
|
498
|
+
original_goal: taskAnalysis.current_goal,
|
|
499
|
+
raw_user_prompt: rawUserPrompt,
|
|
500
|
+
constraints: taskAnalysis.constraints || [],
|
|
501
|
+
task_type: 'main',
|
|
502
|
+
});
|
|
503
|
+
activeSessionId = newSessionId;
|
|
504
|
+
activeSessions.set(newSessionId, {
|
|
505
|
+
sessionId: newSessionId,
|
|
506
|
+
promptCount: 1,
|
|
507
|
+
projectPath: sessionInfo.projectPath,
|
|
508
|
+
});
|
|
509
|
+
logger.info({ msg: 'Created new task session', sessionId: newSessionId.substring(0, 8) });
|
|
510
|
+
taskLog('ORCHESTRATION_NEW_TASK', {
|
|
511
|
+
sessionId: newSessionId,
|
|
512
|
+
goal: taskAnalysis.current_goal,
|
|
513
|
+
constraintsCount: (taskAnalysis.constraints || []).length,
|
|
514
|
+
});
|
|
515
|
+
// Q&A auto-save
|
|
516
|
+
if (taskAnalysis.task_type === 'information' && textContent.length > 100 && actions.length === 0) {
|
|
517
|
+
logger.info({ msg: 'Q&A detected (pure text) - saving immediately', sessionId: newSessionId.substring(0, 8) });
|
|
518
|
+
taskLog('QA_AUTO_SAVE', {
|
|
519
|
+
sessionId: newSessionId,
|
|
520
|
+
goal: taskAnalysis.current_goal,
|
|
521
|
+
responseLength: textContent.length,
|
|
522
|
+
toolCalls: 0,
|
|
523
|
+
});
|
|
524
|
+
updateSessionState(newSessionId, {
|
|
525
|
+
final_response: textContent.substring(0, 10000),
|
|
526
|
+
});
|
|
527
|
+
await saveToTeamMemory(newSessionId, 'complete', taskAnalysis.task_type, requestHeaders);
|
|
528
|
+
markSessionCompleted(newSessionId);
|
|
529
|
+
}
|
|
530
|
+
else if (taskAnalysis.task_type === 'information' && actions.length > 0) {
|
|
531
|
+
logger.info({ msg: 'Q&A with tool calls - waiting for completion', sessionId: newSessionId.substring(0, 8), toolCalls: actions.length });
|
|
532
|
+
taskLog('QA_DEFERRED', {
|
|
533
|
+
sessionId: newSessionId,
|
|
534
|
+
goal: taskAnalysis.current_goal,
|
|
535
|
+
toolCalls: actions.length,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
case 'subtask': {
|
|
541
|
+
const parentId = sessionInfo.currentSession?.session_id || taskAnalysis.parent_task_id;
|
|
542
|
+
const subtaskId = randomUUID();
|
|
543
|
+
activeSession = createSessionState({
|
|
544
|
+
session_id: subtaskId,
|
|
545
|
+
project_path: sessionInfo.projectPath,
|
|
546
|
+
original_goal: taskAnalysis.current_goal,
|
|
547
|
+
raw_user_prompt: rawUserPrompt,
|
|
548
|
+
constraints: taskAnalysis.constraints || [],
|
|
549
|
+
task_type: 'subtask',
|
|
550
|
+
parent_session_id: parentId,
|
|
551
|
+
});
|
|
552
|
+
activeSessionId = subtaskId;
|
|
553
|
+
activeSessions.set(subtaskId, {
|
|
554
|
+
sessionId: subtaskId,
|
|
555
|
+
promptCount: 1,
|
|
556
|
+
projectPath: sessionInfo.projectPath,
|
|
557
|
+
});
|
|
558
|
+
logger.info({ msg: 'Created subtask session', sessionId: subtaskId.substring(0, 8), parent: parentId?.substring(0, 8) });
|
|
559
|
+
taskLog('ORCHESTRATION_SUBTASK', {
|
|
560
|
+
sessionId: subtaskId,
|
|
561
|
+
parentId: parentId || 'none',
|
|
562
|
+
goal: taskAnalysis.current_goal,
|
|
563
|
+
});
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case 'parallel_task': {
|
|
567
|
+
const parentId = sessionInfo.currentSession?.session_id || taskAnalysis.parent_task_id;
|
|
568
|
+
const parallelId = randomUUID();
|
|
569
|
+
activeSession = createSessionState({
|
|
570
|
+
session_id: parallelId,
|
|
571
|
+
project_path: sessionInfo.projectPath,
|
|
572
|
+
original_goal: taskAnalysis.current_goal,
|
|
573
|
+
raw_user_prompt: rawUserPrompt,
|
|
574
|
+
constraints: taskAnalysis.constraints || [],
|
|
575
|
+
task_type: 'parallel',
|
|
576
|
+
parent_session_id: parentId,
|
|
577
|
+
});
|
|
578
|
+
activeSessionId = parallelId;
|
|
579
|
+
activeSessions.set(parallelId, {
|
|
580
|
+
sessionId: parallelId,
|
|
581
|
+
promptCount: 1,
|
|
582
|
+
projectPath: sessionInfo.projectPath,
|
|
583
|
+
});
|
|
584
|
+
logger.info({ msg: 'Created parallel task session', sessionId: parallelId.substring(0, 8), parent: parentId?.substring(0, 8) });
|
|
585
|
+
taskLog('ORCHESTRATION_PARALLEL', {
|
|
586
|
+
sessionId: parallelId,
|
|
587
|
+
parentId: parentId || 'none',
|
|
588
|
+
goal: taskAnalysis.current_goal,
|
|
589
|
+
});
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
case 'task_complete': {
|
|
593
|
+
if (sessionInfo.currentSession) {
|
|
594
|
+
try {
|
|
595
|
+
if (taskAnalysis.current_goal && !sessionInfo.currentSession.original_goal) {
|
|
596
|
+
updateSessionState(sessionInfo.currentSession.session_id, {
|
|
597
|
+
original_goal: taskAnalysis.current_goal,
|
|
598
|
+
});
|
|
599
|
+
sessionInfo.currentSession.original_goal = taskAnalysis.current_goal;
|
|
600
|
+
}
|
|
601
|
+
updateSessionState(sessionInfo.currentSession.session_id, {
|
|
602
|
+
final_response: textContent.substring(0, 10000),
|
|
603
|
+
});
|
|
604
|
+
await saveToTeamMemory(sessionInfo.currentSession.session_id, 'complete', taskAnalysis.task_type, requestHeaders);
|
|
605
|
+
markSessionCompleted(sessionInfo.currentSession.session_id);
|
|
606
|
+
activeSessions.delete(sessionInfo.currentSession.session_id);
|
|
607
|
+
lastDriftResults.delete(sessionInfo.currentSession.session_id);
|
|
608
|
+
taskLog('ORCHESTRATION_TASK_COMPLETE', {
|
|
609
|
+
sessionId: sessionInfo.currentSession.session_id,
|
|
610
|
+
goal: sessionInfo.currentSession.original_goal,
|
|
611
|
+
});
|
|
612
|
+
if (taskAnalysis.task_type === 'planning') {
|
|
613
|
+
try {
|
|
614
|
+
const allSteps = getValidatedSteps(sessionInfo.currentSession.session_id);
|
|
615
|
+
const planSummary = await generateSessionSummary(sessionInfo.currentSession, allSteps, 2000, requestHeaders);
|
|
616
|
+
setPendingPlanClear({
|
|
617
|
+
projectPath: sessionInfo.projectPath,
|
|
618
|
+
summary: planSummary,
|
|
619
|
+
});
|
|
620
|
+
logger.info({
|
|
621
|
+
msg: 'PLANNING_CLEAR triggered',
|
|
622
|
+
sessionId: sessionInfo.currentSession.session_id.substring(0, 8),
|
|
623
|
+
summaryLen: planSummary.length,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
// Silent fail - planning CLEAR is optional
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
logger.info({ msg: 'Task complete - saved to team memory, marked completed' });
|
|
631
|
+
}
|
|
632
|
+
catch (err) {
|
|
633
|
+
logger.info({ msg: 'Failed to save completed task', error: String(err) });
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
else if (textContent.length > 100) {
|
|
637
|
+
// Instant complete
|
|
638
|
+
try {
|
|
639
|
+
const newSessionId = randomUUID();
|
|
640
|
+
const instantSession = createSessionState({
|
|
641
|
+
session_id: newSessionId,
|
|
642
|
+
project_path: sessionInfo.projectPath,
|
|
643
|
+
original_goal: taskAnalysis.current_goal || '',
|
|
644
|
+
raw_user_prompt: rawUserPrompt,
|
|
645
|
+
task_type: 'main',
|
|
646
|
+
});
|
|
647
|
+
updateSessionState(newSessionId, {
|
|
648
|
+
final_response: textContent.substring(0, 10000),
|
|
649
|
+
});
|
|
650
|
+
await saveToTeamMemory(newSessionId, 'complete', taskAnalysis.task_type, requestHeaders);
|
|
651
|
+
markSessionCompleted(newSessionId);
|
|
652
|
+
logger.info({ msg: 'Instant complete - new task saved immediately', sessionId: newSessionId.substring(0, 8) });
|
|
653
|
+
taskLog('ORCHESTRATION_TASK_COMPLETE', {
|
|
654
|
+
sessionId: newSessionId,
|
|
655
|
+
goal: taskAnalysis.current_goal || '',
|
|
656
|
+
source: 'instant_complete',
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
logger.info({ msg: 'Failed to save instant complete task', error: String(err) });
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
case 'subtask_complete': {
|
|
666
|
+
if (sessionInfo.currentSession) {
|
|
667
|
+
const parentId = sessionInfo.currentSession.parent_session_id;
|
|
668
|
+
try {
|
|
669
|
+
await saveToTeamMemory(sessionInfo.currentSession.session_id, 'complete', taskAnalysis.task_type, requestHeaders);
|
|
670
|
+
markSessionCompleted(sessionInfo.currentSession.session_id);
|
|
671
|
+
activeSessions.delete(sessionInfo.currentSession.session_id);
|
|
672
|
+
lastDriftResults.delete(sessionInfo.currentSession.session_id);
|
|
673
|
+
if (parentId) {
|
|
674
|
+
const parentSession = getSessionState(parentId);
|
|
675
|
+
if (parentSession) {
|
|
676
|
+
activeSessionId = parentId;
|
|
677
|
+
activeSession = parentSession;
|
|
678
|
+
logger.info({ msg: 'Subtask complete - returning to parent', parent: parentId.substring(0, 8) });
|
|
679
|
+
taskLog('ORCHESTRATION_SUBTASK_COMPLETE', {
|
|
680
|
+
sessionId: sessionInfo.currentSession.session_id,
|
|
681
|
+
parentId: parentId,
|
|
682
|
+
goal: sessionInfo.currentSession.original_goal,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
catch (err) {
|
|
688
|
+
logger.info({ msg: 'Failed to save completed subtask', error: String(err) });
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
logger.info({ msg: 'Task analysis failed, creating fallback session', error: String(error) });
|
|
697
|
+
if (!sessionInfo.currentSession) {
|
|
698
|
+
const newSessionId = randomUUID();
|
|
699
|
+
activeSession = createSessionState({
|
|
700
|
+
session_id: newSessionId,
|
|
701
|
+
project_path: sessionInfo.projectPath,
|
|
702
|
+
original_goal: latestUserMessage.substring(0, 200),
|
|
703
|
+
raw_user_prompt: rawUserPrompt,
|
|
704
|
+
constraints: [],
|
|
705
|
+
task_type: 'main',
|
|
706
|
+
});
|
|
707
|
+
activeSessionId = newSessionId;
|
|
708
|
+
taskLog('FALLBACK_SESSION_CREATED', {
|
|
709
|
+
sessionId: newSessionId,
|
|
710
|
+
reason: 'task_analysis_failed',
|
|
711
|
+
goal: latestUserMessage.substring(0, 80),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// Token usage and CLEAR mode
|
|
717
|
+
const usage = adapter.extractUsage(response);
|
|
718
|
+
const actualContextSize = usage.cacheCreation + usage.cacheRead;
|
|
719
|
+
if (activeSession) {
|
|
720
|
+
updateTokenCount(activeSessionId, actualContextSize);
|
|
721
|
+
}
|
|
722
|
+
logger.info({
|
|
723
|
+
msg: 'Token usage',
|
|
724
|
+
input: usage.inputTokens,
|
|
725
|
+
output: usage.outputTokens,
|
|
726
|
+
total: usage.totalTokens,
|
|
727
|
+
cacheCreation: usage.cacheCreation,
|
|
728
|
+
cacheRead: usage.cacheRead,
|
|
729
|
+
actualContextSize,
|
|
730
|
+
activeSession: activeSessionId.substring(0, 8),
|
|
731
|
+
});
|
|
732
|
+
// CLEAR mode pre-compute
|
|
733
|
+
const preComputeThreshold = Math.floor(config.TOKEN_CLEAR_THRESHOLD * 0.85);
|
|
734
|
+
if (activeSession &&
|
|
735
|
+
actualContextSize > preComputeThreshold &&
|
|
736
|
+
!activeSession.pending_clear_summary) {
|
|
737
|
+
const allSteps = getValidatedSteps(activeSessionId);
|
|
738
|
+
generateSessionSummary(activeSession, allSteps, 15000, requestHeaders).then(summary => {
|
|
739
|
+
updateSessionState(activeSessionId, { pending_clear_summary: summary });
|
|
740
|
+
logger.info({
|
|
741
|
+
msg: 'CLEAR summary pre-computed',
|
|
742
|
+
actualContextSize,
|
|
743
|
+
threshold: preComputeThreshold,
|
|
744
|
+
summaryLength: summary.length,
|
|
745
|
+
});
|
|
746
|
+
}).catch(err => {
|
|
747
|
+
logger.info({ msg: 'CLEAR summary generation failed', error: String(err) });
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
// Capture final_response
|
|
751
|
+
if (isEndTurn && textContent.length > 100 && activeSessionId) {
|
|
752
|
+
updateSessionState(activeSessionId, {
|
|
753
|
+
final_response: textContent.substring(0, 10000),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
if (actions.length === 0) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const toolNames = actions.map(a => a.toolName);
|
|
760
|
+
logger.info({
|
|
761
|
+
msg: 'Actions parsed',
|
|
762
|
+
count: actions.length,
|
|
763
|
+
tools: toolNames,
|
|
764
|
+
});
|
|
765
|
+
// Recovery alignment check
|
|
766
|
+
if (activeSession && activeSession.waiting_for_recovery) {
|
|
767
|
+
const lastDrift = lastDriftResults.get(activeSessionId);
|
|
768
|
+
const recoveryPlan = lastDrift?.recoverySteps ? { steps: lastDrift.recoverySteps } : undefined;
|
|
769
|
+
for (const action of actions) {
|
|
770
|
+
const alignment = checkRecoveryAlignment({ actionType: action.actionType, files: action.files, command: action.command }, recoveryPlan, activeSession);
|
|
771
|
+
if (alignment.aligned) {
|
|
772
|
+
updateSessionMode(activeSessionId, 'normal');
|
|
773
|
+
markWaitingForRecovery(activeSessionId, false);
|
|
774
|
+
updateSessionState(activeSessionId, { escalation_count: 0 });
|
|
775
|
+
lastDriftResults.delete(activeSessionId);
|
|
776
|
+
logger.info({
|
|
777
|
+
msg: 'Recovery alignment SUCCESS - resuming normal mode',
|
|
778
|
+
reason: alignment.reason,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
incrementEscalation(activeSessionId);
|
|
783
|
+
logger.info({
|
|
784
|
+
msg: 'Recovery alignment FAILED - escalating',
|
|
785
|
+
reason: alignment.reason,
|
|
786
|
+
escalation: activeSession.escalation_count + 1,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
// Drift check
|
|
792
|
+
let driftScore;
|
|
793
|
+
let skipSteps = false;
|
|
794
|
+
const memSessionInfo = activeSessions.get(activeSessionId);
|
|
795
|
+
const promptCount = memSessionInfo?.promptCount || sessionInfo.promptCount;
|
|
796
|
+
const recentStepsForCheck = activeSessionId ? getRecentSteps(activeSessionId, 10) : [];
|
|
797
|
+
const hasFileModifications = recentStepsForCheck.some(s => s.action_type === 'edit' || s.action_type === 'write');
|
|
798
|
+
const hasValidGoal = activeSession?.original_goal &&
|
|
799
|
+
activeSession.original_goal.length > 10 &&
|
|
800
|
+
!activeSession.original_goal.includes('the original task');
|
|
801
|
+
if (hasValidGoal && hasFileModifications && promptCount % config.DRIFT_CHECK_INTERVAL === 0) {
|
|
802
|
+
if (activeSession) {
|
|
803
|
+
const driftResult = await checkDrift({ sessionState: activeSession, recentSteps: recentStepsForCheck, latestUserMessage }, requestHeaders);
|
|
804
|
+
lastDriftResults.set(activeSessionId, driftResult);
|
|
805
|
+
driftScore = driftResult.score;
|
|
806
|
+
skipSteps = shouldSkipSteps(driftScore);
|
|
807
|
+
logger.info({
|
|
808
|
+
msg: 'Drift check',
|
|
809
|
+
score: driftResult.score,
|
|
810
|
+
type: driftResult.driftType,
|
|
811
|
+
diagnostic: driftResult.diagnostic,
|
|
812
|
+
});
|
|
813
|
+
const correctionLevel = scoreToCorrectionLevel(driftScore);
|
|
814
|
+
const currentEscalation = activeSession.escalation_count || 0;
|
|
815
|
+
const maxCorrectionAttempts = 2;
|
|
816
|
+
const shouldCorrect = correctionLevel && currentEscalation < maxCorrectionAttempts;
|
|
817
|
+
if (shouldCorrect && (correctionLevel === 'intervene' || correctionLevel === 'halt')) {
|
|
818
|
+
updateSessionMode(activeSessionId, 'drifted');
|
|
819
|
+
markWaitingForRecovery(activeSessionId, true);
|
|
820
|
+
incrementEscalation(activeSessionId);
|
|
821
|
+
const correction = buildCorrection(driftResult, activeSession, correctionLevel);
|
|
822
|
+
const correctionText = formatCorrectionForInjection(correction);
|
|
823
|
+
updateSessionState(activeSessionId, { pending_correction: correctionText });
|
|
824
|
+
logger.info({
|
|
825
|
+
msg: 'Pre-computed correction saved',
|
|
826
|
+
level: correctionLevel,
|
|
827
|
+
correctionLength: correctionText.length,
|
|
828
|
+
attempt: currentEscalation + 1,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
else if (shouldCorrect && correctionLevel) {
|
|
832
|
+
incrementEscalation(activeSessionId);
|
|
833
|
+
const correction = buildCorrection(driftResult, activeSession, correctionLevel);
|
|
834
|
+
const correctionText = formatCorrectionForInjection(correction);
|
|
835
|
+
updateSessionState(activeSessionId, { pending_correction: correctionText });
|
|
836
|
+
logger.info({
|
|
837
|
+
msg: 'Pre-computed mild correction saved',
|
|
838
|
+
level: correctionLevel,
|
|
839
|
+
attempt: currentEscalation + 1,
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
else if (currentEscalation >= maxCorrectionAttempts && correctionLevel) {
|
|
843
|
+
logger.info({
|
|
844
|
+
msg: 'Max correction attempts reached - giving up',
|
|
845
|
+
attempts: currentEscalation,
|
|
846
|
+
lastScore: driftScore,
|
|
847
|
+
});
|
|
848
|
+
updateSessionMode(activeSessionId, 'normal');
|
|
849
|
+
markWaitingForRecovery(activeSessionId, false);
|
|
850
|
+
updateSessionState(activeSessionId, {
|
|
851
|
+
pending_correction: undefined,
|
|
852
|
+
escalation_count: 0,
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
else if (driftScore >= 5) {
|
|
856
|
+
updateSessionMode(activeSessionId, 'normal');
|
|
857
|
+
markWaitingForRecovery(activeSessionId, false);
|
|
858
|
+
lastDriftResults.delete(activeSessionId);
|
|
859
|
+
updateSessionState(activeSessionId, {
|
|
860
|
+
pending_correction: undefined,
|
|
861
|
+
escalation_count: 0,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
updateLastChecked(activeSessionId, Date.now());
|
|
865
|
+
if (skipSteps) {
|
|
866
|
+
for (const action of actions) {
|
|
867
|
+
logDriftEvent({
|
|
868
|
+
session_id: activeSessionId,
|
|
869
|
+
action_type: action.actionType,
|
|
870
|
+
files: action.files,
|
|
871
|
+
drift_score: driftScore,
|
|
872
|
+
drift_reason: driftResult.diagnostic,
|
|
873
|
+
recovery_plan: driftResult.recoverySteps ? { steps: driftResult.recoverySteps } : undefined,
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
logger.info({
|
|
877
|
+
msg: 'Actions logged to drift_log (skipped steps)',
|
|
878
|
+
reason: 'score < 5',
|
|
879
|
+
});
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// Save steps
|
|
885
|
+
let previousReasoning = null;
|
|
886
|
+
for (const action of actions) {
|
|
887
|
+
const currentReasoning = textContent.substring(0, 1000);
|
|
888
|
+
const isDuplicate = currentReasoning === previousReasoning;
|
|
889
|
+
const isKeyDecision = !isDuplicate && detectKeyDecision(action, textContent);
|
|
890
|
+
createStep({
|
|
891
|
+
session_id: activeSessionId,
|
|
892
|
+
action_type: action.actionType,
|
|
893
|
+
files: action.files,
|
|
894
|
+
folders: action.folders,
|
|
895
|
+
command: action.command,
|
|
896
|
+
reasoning: isDuplicate ? undefined : currentReasoning,
|
|
897
|
+
drift_score: driftScore,
|
|
898
|
+
is_validated: !skipSteps,
|
|
899
|
+
is_key_decision: isKeyDecision,
|
|
900
|
+
});
|
|
901
|
+
previousReasoning = currentReasoning;
|
|
902
|
+
if (isKeyDecision) {
|
|
903
|
+
logger.info({
|
|
904
|
+
msg: 'Key decision detected',
|
|
905
|
+
actionType: action.actionType,
|
|
906
|
+
files: action.files.slice(0, 3),
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// Helper functions
|
|
912
|
+
function getMessages(body) {
|
|
913
|
+
const b = body;
|
|
914
|
+
if (Array.isArray(b.messages))
|
|
915
|
+
return b.messages;
|
|
916
|
+
if (Array.isArray(b.input))
|
|
917
|
+
return b.input;
|
|
918
|
+
return [];
|
|
919
|
+
}
|
|
920
|
+
function getMessageCount(body) {
|
|
921
|
+
return getMessages(body).length;
|
|
922
|
+
}
|
|
923
|
+
function getAssistantContent(response, adapter) {
|
|
924
|
+
const blocks = adapter.getToolUseBlocks(response);
|
|
925
|
+
if (blocks.length > 0) {
|
|
926
|
+
// Return the content array for Claude, or serialized for Codex
|
|
927
|
+
const r = response;
|
|
928
|
+
if (r.content)
|
|
929
|
+
return r.content;
|
|
930
|
+
if (r.output)
|
|
931
|
+
return r.output;
|
|
932
|
+
}
|
|
933
|
+
return [];
|
|
934
|
+
}
|
|
935
|
+
function detectKeyDecision(action, reasoning) {
|
|
936
|
+
if (action.actionType === 'edit' || action.actionType === 'write') {
|
|
937
|
+
return true;
|
|
938
|
+
}
|
|
939
|
+
const decisionKeywords = [
|
|
940
|
+
'decision', 'decided', 'chose', 'chosen', 'selected', 'picked',
|
|
941
|
+
'approach', 'strategy', 'solution', 'implementation',
|
|
942
|
+
'because', 'reason', 'rationale', 'trade-off', 'tradeoff',
|
|
943
|
+
'instead of', 'rather than', 'prefer', 'opted',
|
|
944
|
+
'conclusion', 'determined', 'resolved'
|
|
945
|
+
];
|
|
946
|
+
const reasoningLower = reasoning.toLowerCase();
|
|
947
|
+
const hasDecisionKeyword = decisionKeywords.some(kw => reasoningLower.includes(kw));
|
|
948
|
+
if (hasDecisionKeyword && reasoning.length > 200) {
|
|
949
|
+
return true;
|
|
950
|
+
}
|
|
951
|
+
return false;
|
|
952
|
+
}
|
|
953
|
+
// Export for server.ts startup/cleanup
|
|
954
|
+
export { activeSessions, lastDriftResults, lastMessageCount };
|