claudia-orchestrator 0.1.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/LICENSE +201 -0
- package/README.md +109 -0
- package/dist/cli-parser.d.ts +11 -0
- package/dist/cli-parser.d.ts.map +1 -0
- package/dist/cli-parser.js +57 -0
- package/dist/cli-parser.js.map +1 -0
- package/dist/cui-server.d.ts +69 -0
- package/dist/cui-server.d.ts.map +1 -0
- package/dist/cui-server.js +705 -0
- package/dist/cui-server.js.map +1 -0
- package/dist/mcp-server/claudia-tools.d.ts +15 -0
- package/dist/mcp-server/claudia-tools.d.ts.map +1 -0
- package/dist/mcp-server/claudia-tools.js +366 -0
- package/dist/mcp-server/claudia-tools.js.map +1 -0
- package/dist/mcp-server/index.d.ts +3 -0
- package/dist/mcp-server/index.d.ts.map +1 -0
- package/dist/mcp-server/index.js +176 -0
- package/dist/mcp-server/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +18 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +136 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors-setup.d.ts +7 -0
- package/dist/middleware/cors-setup.d.ts.map +1 -0
- package/dist/middleware/cors-setup.js +8 -0
- package/dist/middleware/cors-setup.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +4 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +27 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/query-parser.d.ts +11 -0
- package/dist/middleware/query-parser.d.ts.map +1 -0
- package/dist/middleware/query-parser.js +68 -0
- package/dist/middleware/query-parser.js.map +1 -0
- package/dist/middleware/request-logger.d.ts +4 -0
- package/dist/middleware/request-logger.d.ts.map +1 -0
- package/dist/middleware/request-logger.js +29 -0
- package/dist/middleware/request-logger.js.map +1 -0
- package/dist/process-daemon/index.d.ts +14 -0
- package/dist/process-daemon/index.d.ts.map +1 -0
- package/dist/process-daemon/index.js +51 -0
- package/dist/process-daemon/index.js.map +1 -0
- package/dist/process-daemon/process-daemon.d.ts +78 -0
- package/dist/process-daemon/process-daemon.d.ts.map +1 -0
- package/dist/process-daemon/process-daemon.js +568 -0
- package/dist/process-daemon/process-daemon.js.map +1 -0
- package/dist/process-daemon/process-manager-client.d.ts +108 -0
- package/dist/process-daemon/process-manager-client.d.ts.map +1 -0
- package/dist/process-daemon/process-manager-client.js +314 -0
- package/dist/process-daemon/process-manager-client.js.map +1 -0
- package/dist/process-daemon/process-manager-interface.d.ts +47 -0
- package/dist/process-daemon/process-manager-interface.d.ts.map +1 -0
- package/dist/process-daemon/process-manager-interface.js +8 -0
- package/dist/process-daemon/process-manager-interface.js.map +1 -0
- package/dist/process-daemon/test-daemon.d.ts +12 -0
- package/dist/process-daemon/test-daemon.d.ts.map +1 -0
- package/dist/process-daemon/test-daemon.js +81 -0
- package/dist/process-daemon/test-daemon.js.map +1 -0
- package/dist/process-daemon/types.d.ts +85 -0
- package/dist/process-daemon/types.d.ts.map +1 -0
- package/dist/process-daemon/types.js +8 -0
- package/dist/process-daemon/types.js.map +1 -0
- package/dist/routes/claudia.routes.d.ts +10 -0
- package/dist/routes/claudia.routes.d.ts.map +1 -0
- package/dist/routes/claudia.routes.js +123 -0
- package/dist/routes/claudia.routes.js.map +1 -0
- package/dist/routes/config.routes.d.ts +4 -0
- package/dist/routes/config.routes.d.ts.map +1 -0
- package/dist/routes/config.routes.js +27 -0
- package/dist/routes/config.routes.js.map +1 -0
- package/dist/routes/conversation.routes.d.ts +8 -0
- package/dist/routes/conversation.routes.d.ts.map +1 -0
- package/dist/routes/conversation.routes.js +870 -0
- package/dist/routes/conversation.routes.js.map +1 -0
- package/dist/routes/filesystem.routes.d.ts +4 -0
- package/dist/routes/filesystem.routes.d.ts.map +1 -0
- package/dist/routes/filesystem.routes.js +86 -0
- package/dist/routes/filesystem.routes.js.map +1 -0
- package/dist/routes/gemini.routes.d.ts +4 -0
- package/dist/routes/gemini.routes.d.ts.map +1 -0
- package/dist/routes/gemini.routes.js +93 -0
- package/dist/routes/gemini.routes.js.map +1 -0
- package/dist/routes/insights.routes.d.ts +17 -0
- package/dist/routes/insights.routes.d.ts.map +1 -0
- package/dist/routes/insights.routes.js +417 -0
- package/dist/routes/insights.routes.js.map +1 -0
- package/dist/routes/license.routes.d.ts +3 -0
- package/dist/routes/license.routes.d.ts.map +1 -0
- package/dist/routes/license.routes.js +111 -0
- package/dist/routes/license.routes.js.map +1 -0
- package/dist/routes/log.routes.d.ts +3 -0
- package/dist/routes/log.routes.d.ts.map +1 -0
- package/dist/routes/log.routes.js +65 -0
- package/dist/routes/log.routes.js.map +1 -0
- package/dist/routes/notifications.routes.d.ts +4 -0
- package/dist/routes/notifications.routes.d.ts.map +1 -0
- package/dist/routes/notifications.routes.js +71 -0
- package/dist/routes/notifications.routes.js.map +1 -0
- package/dist/routes/permission.routes.d.ts +4 -0
- package/dist/routes/permission.routes.d.ts.map +1 -0
- package/dist/routes/permission.routes.js +116 -0
- package/dist/routes/permission.routes.js.map +1 -0
- package/dist/routes/question.routes.d.ts +8 -0
- package/dist/routes/question.routes.d.ts.map +1 -0
- package/dist/routes/question.routes.js +82 -0
- package/dist/routes/question.routes.js.map +1 -0
- package/dist/routes/streaming.routes.d.ts +4 -0
- package/dist/routes/streaming.routes.d.ts.map +1 -0
- package/dist/routes/streaming.routes.js +28 -0
- package/dist/routes/streaming.routes.js.map +1 -0
- package/dist/routes/system.routes.d.ts +5 -0
- package/dist/routes/system.routes.d.ts.map +1 -0
- package/dist/routes/system.routes.js +103 -0
- package/dist/routes/system.routes.js.map +1 -0
- package/dist/routes/working-directories.routes.d.ts +4 -0
- package/dist/routes/working-directories.routes.d.ts.map +1 -0
- package/dist/routes/working-directories.routes.js +25 -0
- package/dist/routes/working-directories.routes.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +34 -0
- package/dist/server.js.map +1 -0
- package/dist/services/ToolMetricsService.d.ts +53 -0
- package/dist/services/ToolMetricsService.d.ts.map +1 -0
- package/dist/services/ToolMetricsService.js +230 -0
- package/dist/services/ToolMetricsService.js.map +1 -0
- package/dist/services/anthropic-service.d.ts +186 -0
- package/dist/services/anthropic-service.d.ts.map +1 -0
- package/dist/services/anthropic-service.js +1132 -0
- package/dist/services/anthropic-service.js.map +1 -0
- package/dist/services/claude-history-reader.d.ts +126 -0
- package/dist/services/claude-history-reader.d.ts.map +1 -0
- package/dist/services/claude-history-reader.js +717 -0
- package/dist/services/claude-history-reader.js.map +1 -0
- package/dist/services/claude-process-manager.d.ts +108 -0
- package/dist/services/claude-process-manager.d.ts.map +1 -0
- package/dist/services/claude-process-manager.js +909 -0
- package/dist/services/claude-process-manager.js.map +1 -0
- package/dist/services/claude-router-service.d.ts +19 -0
- package/dist/services/claude-router-service.d.ts.map +1 -0
- package/dist/services/claude-router-service.js +160 -0
- package/dist/services/claude-router-service.js.map +1 -0
- package/dist/services/claudia-service.d.ts +77 -0
- package/dist/services/claudia-service.d.ts.map +1 -0
- package/dist/services/claudia-service.js +194 -0
- package/dist/services/claudia-service.js.map +1 -0
- package/dist/services/commands-service.d.ts +18 -0
- package/dist/services/commands-service.d.ts.map +1 -0
- package/dist/services/commands-service.js +76 -0
- package/dist/services/commands-service.js.map +1 -0
- package/dist/services/config-service.d.ts +68 -0
- package/dist/services/config-service.d.ts.map +1 -0
- package/dist/services/config-service.js +429 -0
- package/dist/services/config-service.js.map +1 -0
- package/dist/services/conversation-cache.d.ts +86 -0
- package/dist/services/conversation-cache.d.ts.map +1 -0
- package/dist/services/conversation-cache.js +235 -0
- package/dist/services/conversation-cache.js.map +1 -0
- package/dist/services/conversation-status-manager.d.ts +98 -0
- package/dist/services/conversation-status-manager.d.ts.map +1 -0
- package/dist/services/conversation-status-manager.js +295 -0
- package/dist/services/conversation-status-manager.js.map +1 -0
- package/dist/services/cost-tracker.d.ts +87 -0
- package/dist/services/cost-tracker.d.ts.map +1 -0
- package/dist/services/cost-tracker.js +335 -0
- package/dist/services/cost-tracker.js.map +1 -0
- package/dist/services/file-system-service.d.ts +61 -0
- package/dist/services/file-system-service.d.ts.map +1 -0
- package/dist/services/file-system-service.js +348 -0
- package/dist/services/file-system-service.js.map +1 -0
- package/dist/services/gemini-service.d.ts +72 -0
- package/dist/services/gemini-service.d.ts.map +1 -0
- package/dist/services/gemini-service.js +431 -0
- package/dist/services/gemini-service.js.map +1 -0
- package/dist/services/insight-queue.d.ts +99 -0
- package/dist/services/insight-queue.d.ts.map +1 -0
- package/dist/services/insight-queue.js +277 -0
- package/dist/services/insight-queue.js.map +1 -0
- package/dist/services/insights-service.d.ts +102 -0
- package/dist/services/insights-service.d.ts.map +1 -0
- package/dist/services/insights-service.js +1152 -0
- package/dist/services/insights-service.js.map +1 -0
- package/dist/services/json-lines-parser.d.ts +19 -0
- package/dist/services/json-lines-parser.d.ts.map +1 -0
- package/dist/services/json-lines-parser.js +56 -0
- package/dist/services/json-lines-parser.js.map +1 -0
- package/dist/services/license-service.d.ts +69 -0
- package/dist/services/license-service.d.ts.map +1 -0
- package/dist/services/license-service.js +330 -0
- package/dist/services/license-service.js.map +1 -0
- package/dist/services/log-formatter.d.ts +5 -0
- package/dist/services/log-formatter.d.ts.map +1 -0
- package/dist/services/log-formatter.js +77 -0
- package/dist/services/log-formatter.js.map +1 -0
- package/dist/services/log-stream-buffer.d.ts +11 -0
- package/dist/services/log-stream-buffer.d.ts.map +1 -0
- package/dist/services/log-stream-buffer.js +36 -0
- package/dist/services/log-stream-buffer.js.map +1 -0
- package/dist/services/logger.d.ts +71 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +215 -0
- package/dist/services/logger.js.map +1 -0
- package/dist/services/mcp-config-generator.d.ts +32 -0
- package/dist/services/mcp-config-generator.d.ts.map +1 -0
- package/dist/services/mcp-config-generator.js +126 -0
- package/dist/services/mcp-config-generator.js.map +1 -0
- package/dist/services/message-filter.d.ts +22 -0
- package/dist/services/message-filter.d.ts.map +1 -0
- package/dist/services/message-filter.js +57 -0
- package/dist/services/message-filter.js.map +1 -0
- package/dist/services/notification-service.d.ts +45 -0
- package/dist/services/notification-service.d.ts.map +1 -0
- package/dist/services/notification-service.js +184 -0
- package/dist/services/notification-service.js.map +1 -0
- package/dist/services/permission-tracker.d.ts +67 -0
- package/dist/services/permission-tracker.d.ts.map +1 -0
- package/dist/services/permission-tracker.js +161 -0
- package/dist/services/permission-tracker.js.map +1 -0
- package/dist/services/process-manager-factory.d.ts +81 -0
- package/dist/services/process-manager-factory.d.ts.map +1 -0
- package/dist/services/process-manager-factory.js +211 -0
- package/dist/services/process-manager-factory.js.map +1 -0
- package/dist/services/question-tracker.d.ts +47 -0
- package/dist/services/question-tracker.d.ts.map +1 -0
- package/dist/services/question-tracker.js +105 -0
- package/dist/services/question-tracker.js.map +1 -0
- package/dist/services/session-activity-watcher.d.ts +33 -0
- package/dist/services/session-activity-watcher.d.ts.map +1 -0
- package/dist/services/session-activity-watcher.js +194 -0
- package/dist/services/session-activity-watcher.js.map +1 -0
- package/dist/services/session-info-service.d.ts +228 -0
- package/dist/services/session-info-service.d.ts.map +1 -0
- package/dist/services/session-info-service.js +920 -0
- package/dist/services/session-info-service.js.map +1 -0
- package/dist/services/session-insights-service.d.ts +119 -0
- package/dist/services/session-insights-service.d.ts.map +1 -0
- package/dist/services/session-insights-service.js +889 -0
- package/dist/services/session-insights-service.js.map +1 -0
- package/dist/services/stream-manager.d.ts +62 -0
- package/dist/services/stream-manager.d.ts.map +1 -0
- package/dist/services/stream-manager.js +239 -0
- package/dist/services/stream-manager.js.map +1 -0
- package/dist/services/web-push-service.d.ts +48 -0
- package/dist/services/web-push-service.d.ts.map +1 -0
- package/dist/services/web-push-service.js +186 -0
- package/dist/services/web-push-service.js.map +1 -0
- package/dist/services/working-directories-service.d.ts +19 -0
- package/dist/services/working-directories-service.d.ts.map +1 -0
- package/dist/services/working-directories-service.js +103 -0
- package/dist/services/working-directories-service.js.map +1 -0
- package/dist/types/config.d.ts +111 -0
- package/dist/types/config.d.ts.map +1 -0
- package/dist/types/config.js +14 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/express.d.ts +5 -0
- package/dist/types/express.d.ts.map +1 -0
- package/dist/types/express.js +2 -0
- package/dist/types/express.js.map +1 -0
- package/dist/types/index.d.ts +325 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +18 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/insights.d.ts +99 -0
- package/dist/types/insights.d.ts.map +1 -0
- package/dist/types/insights.js +7 -0
- package/dist/types/insights.js.map +1 -0
- package/dist/types/license.d.ts +70 -0
- package/dist/types/license.d.ts.map +1 -0
- package/dist/types/license.js +5 -0
- package/dist/types/license.js.map +1 -0
- package/dist/types/router-config.d.ts +13 -0
- package/dist/types/router-config.d.ts.map +1 -0
- package/dist/types/router-config.js +2 -0
- package/dist/types/router-config.js.map +1 -0
- package/dist/utils/constants.d.ts +26 -0
- package/dist/utils/constants.d.ts.map +1 -0
- package/dist/utils/constants.js +28 -0
- package/dist/utils/constants.js.map +1 -0
- package/dist/utils/machine-id.d.ts +7 -0
- package/dist/utils/machine-id.d.ts.map +1 -0
- package/dist/utils/machine-id.js +76 -0
- package/dist/utils/machine-id.js.map +1 -0
- package/dist/utils/server-startup.d.ts +13 -0
- package/dist/utils/server-startup.d.ts.map +1 -0
- package/dist/utils/server-startup.js +20 -0
- package/dist/utils/server-startup.js.map +1 -0
- package/dist/web/assets/main-DAc2rjJ2.css +1 -0
- package/dist/web/assets/main-DvlZ02mT.js +137 -0
- package/dist/web/favicon.png +0 -0
- package/dist/web/favicon.svg +22 -0
- package/dist/web/icon-192x192.png +0 -0
- package/dist/web/icon-512x512.png +0 -0
- package/dist/web/index.html +36 -0
- package/dist/web/manifest.json +61 -0
- package/package.json +174 -0
- package/scripts/postinstall.js +30 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
import Anthropic from '@anthropic-ai/sdk';
|
|
2
|
+
import { CUIError, } from '../types/index.js';
|
|
3
|
+
import { createLogger } from '../services/logger.js';
|
|
4
|
+
import { ConfigService } from './config-service.js';
|
|
5
|
+
import { getCostTracker } from './cost-tracker.js';
|
|
6
|
+
const DEFAULT_RETRY_CONFIG = {
|
|
7
|
+
maxRetries: 3,
|
|
8
|
+
baseDelayMs: 1000,
|
|
9
|
+
maxDelayMs: 10000,
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Determines if an error is retryable (transient network/rate limit issues).
|
|
13
|
+
* Non-retryable: auth errors, credit exhaustion, invalid requests.
|
|
14
|
+
*/
|
|
15
|
+
function isRetryableError(error) {
|
|
16
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
17
|
+
const errorName = error instanceof Error ? error.name : '';
|
|
18
|
+
// Non-retryable errors - don't waste retries on these
|
|
19
|
+
if (message.includes('credit balance') || message.includes('billing'))
|
|
20
|
+
return false;
|
|
21
|
+
if (message.includes('invalid_api_key') || message.includes('authentication'))
|
|
22
|
+
return false;
|
|
23
|
+
if (message.includes('invalid_request_error'))
|
|
24
|
+
return false;
|
|
25
|
+
// Retryable: rate limits, overloaded, server errors, network issues
|
|
26
|
+
if (message.includes('rate_limit') || message.includes('overloaded'))
|
|
27
|
+
return true;
|
|
28
|
+
if (message.includes('529') || message.includes('503') || message.includes('500'))
|
|
29
|
+
return true;
|
|
30
|
+
if (errorName === 'APIConnectionError' || message.includes('ECONNRESET'))
|
|
31
|
+
return true;
|
|
32
|
+
if (errorName === 'RateLimitError' || errorName === 'InternalServerError')
|
|
33
|
+
return true;
|
|
34
|
+
// Default: retry on unknown errors (might be transient)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Calculates exponential backoff delay with jitter.
|
|
39
|
+
*/
|
|
40
|
+
function calculateBackoff(attempt, config) {
|
|
41
|
+
const exponentialDelay = config.baseDelayMs * Math.pow(2, attempt);
|
|
42
|
+
const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter
|
|
43
|
+
return Math.min(exponentialDelay + jitter, config.maxDelayMs);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Sleep for a specified duration.
|
|
47
|
+
*/
|
|
48
|
+
function sleep(ms) {
|
|
49
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
50
|
+
}
|
|
51
|
+
// ============================================================================
|
|
52
|
+
// Model Configuration
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Centralized model IDs - update these when Anthropic releases new versions.
|
|
55
|
+
// These are defaults; can be overridden via config.anthropic.models.*
|
|
56
|
+
const DEFAULT_MODELS = {
|
|
57
|
+
/** Full insight generation - Sonnet balances quality/cost well for structured extraction */
|
|
58
|
+
generation: 'claude-sonnet-4-5-20250929',
|
|
59
|
+
/** Quick staleness checks - needs speed over capability */
|
|
60
|
+
quickCheck: 'claude-haiku-4-5-20251001',
|
|
61
|
+
/** Fast patch generation - speed over quality */
|
|
62
|
+
patch: 'claude-haiku-4-5-20251001',
|
|
63
|
+
/** Full patch generation (fallback) - for complex patches when fast path fails */
|
|
64
|
+
fullPatch: 'claude-haiku-4-5-20251001',
|
|
65
|
+
/** Proposition extraction - factual extraction task */
|
|
66
|
+
propositions: 'claude-haiku-4-5-20251001',
|
|
67
|
+
};
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// Shared Prompt Components
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Extracted prompt sections that are reused across multiple LLM calls.
|
|
72
|
+
// Single source of truth for consistent behavior.
|
|
73
|
+
/**
|
|
74
|
+
* CurrentState decision logic - guides the LLM on how to determine session state.
|
|
75
|
+
* Used in: extractSessionInsights, generateCurrentStateOnly, generateInsightsPatch
|
|
76
|
+
*/
|
|
77
|
+
const CURRENT_STATE_DECISION_PROMPT = `Think carefully about what someone monitoring this session needs to know.
|
|
78
|
+
|
|
79
|
+
Before deciding the current state, ask yourself:
|
|
80
|
+
|
|
81
|
+
1. "What is the actual situation right now?"
|
|
82
|
+
- Is the agent actively working on something?
|
|
83
|
+
- Is the agent idle, waiting for the user?
|
|
84
|
+
- Is something blocking progress?
|
|
85
|
+
|
|
86
|
+
2. "What's the last thing Claude communicated to the user?"
|
|
87
|
+
- Did Claude make a request or ask a question?
|
|
88
|
+
- Did Claude present a summary and stop?
|
|
89
|
+
- Did Claude describe next steps that require user action?
|
|
90
|
+
|
|
91
|
+
3. "Is there unfinished business?"
|
|
92
|
+
- Even if implementation is "done", is validation/testing/verification still needed?
|
|
93
|
+
- Did Claude say "ready to test" or "after you do X" - meaning X hasn't happened yet?
|
|
94
|
+
- Or is everything truly complete with no pending actions?
|
|
95
|
+
|
|
96
|
+
4. "What does the dashboard viewer need to understand?"
|
|
97
|
+
- If they're checking in on this session, what's the most accurate summary?
|
|
98
|
+
- What's the one thing they need to know about where this session stands?
|
|
99
|
+
|
|
100
|
+
IMPORTANT: When in doubt between "completed" vs "waiting", err on the side of "waiting".
|
|
101
|
+
It's much worse to say everything is done when user action is still needed, than to imply
|
|
102
|
+
there's pending work when things are actually complete. If there's ANY hint of testing,
|
|
103
|
+
validation, verification, or "after you do X" - that's waiting, not completed.
|
|
104
|
+
|
|
105
|
+
Choose the format and wording that communicates most clearly:
|
|
106
|
+
- Single line summary, or
|
|
107
|
+
- Bullet points (with or without icons)
|
|
108
|
+
|
|
109
|
+
Color guidance:
|
|
110
|
+
- **amber**: waiting for user action, pending decision, question asked
|
|
111
|
+
- **red**: blocked, error preventing progress
|
|
112
|
+
- **emerald**: truly complete - implemented, validated, no pending actions
|
|
113
|
+
- **sky**: actively working, in progress
|
|
114
|
+
|
|
115
|
+
Choose an icon that fits the situation.
|
|
116
|
+
|
|
117
|
+
The right state is the one that accurately communicates the session's reality to someone monitoring from the dashboard.`;
|
|
118
|
+
export class AnthropicService {
|
|
119
|
+
logger;
|
|
120
|
+
client = null;
|
|
121
|
+
// Model configuration - populated from config or defaults
|
|
122
|
+
// Uses explicit mutable type rather than `typeof DEFAULT_MODELS` which is readonly due to `as const`
|
|
123
|
+
models;
|
|
124
|
+
constructor() {
|
|
125
|
+
this.logger = createLogger('AnthropicService');
|
|
126
|
+
this.models = { ...DEFAULT_MODELS };
|
|
127
|
+
}
|
|
128
|
+
async initialize() {
|
|
129
|
+
const config = ConfigService.getInstance().getConfig();
|
|
130
|
+
const apiKey = config.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY;
|
|
131
|
+
if (!apiKey) {
|
|
132
|
+
this.logger.warn('Anthropic API key not configured');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
try {
|
|
136
|
+
this.client = new Anthropic({ apiKey });
|
|
137
|
+
// Load model overrides from config
|
|
138
|
+
const configModels = config.anthropic?.models;
|
|
139
|
+
if (configModels) {
|
|
140
|
+
if (configModels.generation)
|
|
141
|
+
this.models.generation = configModels.generation;
|
|
142
|
+
if (configModels.quickCheck)
|
|
143
|
+
this.models.quickCheck = configModels.quickCheck;
|
|
144
|
+
if (configModels.patch)
|
|
145
|
+
this.models.patch = configModels.patch;
|
|
146
|
+
if (configModels.fullPatch)
|
|
147
|
+
this.models.fullPatch = configModels.fullPatch;
|
|
148
|
+
if (configModels.propositions)
|
|
149
|
+
this.models.propositions = configModels.propositions;
|
|
150
|
+
}
|
|
151
|
+
// Also support legacy config.anthropic.model for generation
|
|
152
|
+
if (config.anthropic?.model) {
|
|
153
|
+
this.models.generation = config.anthropic.model;
|
|
154
|
+
}
|
|
155
|
+
this.logger.info('Anthropic service initialized', { models: this.models });
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
this.logger.error('Failed to initialize Anthropic service', { error });
|
|
159
|
+
throw new CUIError('ANTHROPIC_INIT_ERROR', 'Failed to initialize Anthropic service', 500);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Execute a function with exponential backoff retry on transient errors.
|
|
164
|
+
*/
|
|
165
|
+
async withRetry(operation, fn, config = DEFAULT_RETRY_CONFIG) {
|
|
166
|
+
let lastError;
|
|
167
|
+
for (let attempt = 0; attempt <= config.maxRetries; attempt++) {
|
|
168
|
+
try {
|
|
169
|
+
return await fn();
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
lastError = error;
|
|
173
|
+
// Don't retry non-retryable errors
|
|
174
|
+
if (!isRetryableError(error)) {
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
// Don't retry after max attempts
|
|
178
|
+
if (attempt >= config.maxRetries) {
|
|
179
|
+
this.logger.warn(`${operation}: All ${config.maxRetries} retries exhausted`, {
|
|
180
|
+
error: error instanceof Error ? error.message : String(error)
|
|
181
|
+
});
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
const delayMs = calculateBackoff(attempt, config);
|
|
185
|
+
this.logger.info(`${operation}: Retry ${attempt + 1}/${config.maxRetries} after ${Math.round(delayMs)}ms`, {
|
|
186
|
+
error: error instanceof Error ? error.message : String(error)
|
|
187
|
+
});
|
|
188
|
+
await sleep(delayMs);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
throw lastError;
|
|
192
|
+
}
|
|
193
|
+
async checkHealth() {
|
|
194
|
+
if (!this.client) {
|
|
195
|
+
return {
|
|
196
|
+
status: 'unhealthy',
|
|
197
|
+
message: 'Anthropic API key not configured',
|
|
198
|
+
apiKeyValid: false
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const response = await this.withRetry('checkHealth', () => this.client.messages.create({
|
|
203
|
+
model: this.models.generation,
|
|
204
|
+
max_tokens: 50,
|
|
205
|
+
messages: [{ role: 'user', content: 'Say "ok" and nothing else.' }]
|
|
206
|
+
}));
|
|
207
|
+
const text = response.content[0]?.type === 'text' ? response.content[0].text : null;
|
|
208
|
+
if (text) {
|
|
209
|
+
return {
|
|
210
|
+
status: 'healthy',
|
|
211
|
+
message: 'Anthropic API is accessible',
|
|
212
|
+
apiKeyValid: true
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
status: 'unhealthy',
|
|
217
|
+
message: 'Unexpected response from Anthropic API',
|
|
218
|
+
apiKeyValid: true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
this.logger.error('Health check failed', { error });
|
|
223
|
+
return {
|
|
224
|
+
status: 'unhealthy',
|
|
225
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
226
|
+
apiKeyValid: false
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Log cost for an LLM API call.
|
|
232
|
+
* Call this after each API call to track costs.
|
|
233
|
+
*/
|
|
234
|
+
logCost(response, operation, model, durationMs, sessionId) {
|
|
235
|
+
try {
|
|
236
|
+
const costTracker = getCostTracker();
|
|
237
|
+
costTracker.log({
|
|
238
|
+
sessionId: sessionId || 'unknown',
|
|
239
|
+
operation,
|
|
240
|
+
model,
|
|
241
|
+
inputTokens: response.usage?.input_tokens || 0,
|
|
242
|
+
outputTokens: response.usage?.output_tokens || 0,
|
|
243
|
+
durationMs,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
// Don't let cost logging failures break the main flow
|
|
248
|
+
this.logger.debug('Failed to log cost', { error, operation });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Extract rich, structured session insights using Opus 4.5
|
|
253
|
+
*/
|
|
254
|
+
async extractSessionInsights(conversationText) {
|
|
255
|
+
if (!this.client) {
|
|
256
|
+
throw new CUIError('ANTHROPIC_API_KEY_MISSING', 'Anthropic API key not configured', 400);
|
|
257
|
+
}
|
|
258
|
+
const systemPrompt = `You are analyzing a coding session transcript to extract structured information for a dashboard display.
|
|
259
|
+
|
|
260
|
+
The goal: Someone glancing at this dashboard should immediately understand what this session is about, where it's at, and what's notable.
|
|
261
|
+
|
|
262
|
+
Extract the following:
|
|
263
|
+
|
|
264
|
+
1. CONTEXT - Identity of the session:
|
|
265
|
+
- project: The codebase/repo name
|
|
266
|
+
- area: The specific component/module/domain if applicable (null if general)
|
|
267
|
+
- mission: The overarching goal in one sentence - be specific
|
|
268
|
+
- scope: "minor" (quick fix), "feature" (meaningful addition), "major" (significant change)
|
|
269
|
+
|
|
270
|
+
2. CURRENT STATE - Describe where this coding session stands right now.
|
|
271
|
+
|
|
272
|
+
This is the most important indicator for deciding whether to jump back into a session.
|
|
273
|
+
|
|
274
|
+
${CURRENT_STATE_DECISION_PROMPT.split('\n').map(line => ' ' + line).join('\n')}
|
|
275
|
+
|
|
276
|
+
LANGUAGE RULES:
|
|
277
|
+
- Use "we" to mean the session participants (user + Claude working together)
|
|
278
|
+
- NEVER use "you" or "your" - the person reading this dashboard is NOT the session participant
|
|
279
|
+
- Bad: "your edit", "you found", "waiting for your input"
|
|
280
|
+
- Good: "We made an edit", "We found the issue", "Waiting for input on X"
|
|
281
|
+
|
|
282
|
+
Each currentState has:
|
|
283
|
+
- icon: emoji that fits the situation
|
|
284
|
+
- color: semantic color (amber/red/emerald/sky) based on the guidance above
|
|
285
|
+
- content: Single line summary OR bullet points (with or without icons)
|
|
286
|
+
|
|
287
|
+
3. THEME - A single word capturing the session's current mode/vibe:
|
|
288
|
+
Examples: "debugging", "building", "exploring", "firefighting", "refactoring", "designing", "investigating", "polishing"
|
|
289
|
+
|
|
290
|
+
4. TAGS - Quick categorization:
|
|
291
|
+
- workType: ["implementing", "debugging", "refactoring", "exploring", "designing"]
|
|
292
|
+
- domain: Technical domains touched, e.g., ["UI", "database"], ["API", "auth"]
|
|
293
|
+
- collaboration: "autonomous" | "iterative" | "guided"
|
|
294
|
+
- complexity: "routine" | "tricky" | "gnarly"
|
|
295
|
+
|
|
296
|
+
5. PANELS - Design 3-4 stable semantic panels that capture the essence of this session.
|
|
297
|
+
|
|
298
|
+
PANEL PHILOSOPHY:
|
|
299
|
+
- Panels should be STABLE - they define the semantic shape of the session
|
|
300
|
+
- Update the VALUE when facts change, not the panel itself
|
|
301
|
+
- Only change panels if the session fundamentally pivots
|
|
302
|
+
- Use expressive emoji icons that capture meaning at a glance
|
|
303
|
+
|
|
304
|
+
Each panel has:
|
|
305
|
+
- label: Short semantic label (e.g., "Goal", "Key Constraint", "Approach", "Target User")
|
|
306
|
+
- icon: Single emoji that captures the panel meaning (๐ฏ ๐ง ๐ฌ โ ๏ธ ๐ ๐ก ๐ง ๐ etc.)
|
|
307
|
+
- value: A single concise sentence capturing the current state
|
|
308
|
+
- color: One of: "sky", "purple", "rose", "orange", "emerald", "amber", "red", "teal", "blue", "fuchsia", "slate"
|
|
309
|
+
|
|
310
|
+
Pick colors that match semantic meaning:
|
|
311
|
+
- sky/blue: informational, context, status
|
|
312
|
+
- emerald/teal: success, progress, done
|
|
313
|
+
- amber/orange: warnings, blockers, pending
|
|
314
|
+
- red/rose: errors, critical issues
|
|
315
|
+
- purple/fuchsia: decisions, architecture, design
|
|
316
|
+
- slate: neutral, reference
|
|
317
|
+
|
|
318
|
+
6. MILESTONES - The roadmap of the session:
|
|
319
|
+
|
|
320
|
+
Milestones show the journey: what's done, what's active, AND what's coming.
|
|
321
|
+
|
|
322
|
+
CRITICAL: ALWAYS return 3-5 milestones. Think like a project manager - decompose the mission into steps:
|
|
323
|
+
- What has been accomplished? (done)
|
|
324
|
+
- What's actively being worked on? (in_progress)
|
|
325
|
+
- What logically comes NEXT? (pending) โ This is the most valuable part!
|
|
326
|
+
|
|
327
|
+
FORWARD-LOOKING IS KEY:
|
|
328
|
+
- Don't just report what happened - anticipate what's needed to complete the mission
|
|
329
|
+
- Break down the user's goal into logical steps, even if they weren't explicitly stated
|
|
330
|
+
- Example: If mission is "add dark mode", milestones might be:
|
|
331
|
+
โ [done] Create theme context
|
|
332
|
+
โ [in_progress] Update components to use theme
|
|
333
|
+
โ [pending] Add theme toggle UI
|
|
334
|
+
โ [pending] Persist theme preference
|
|
335
|
+
โ [pending] Test theme switching
|
|
336
|
+
|
|
337
|
+
MILESTONE PHILOSOPHY:
|
|
338
|
+
- Milestones tell the story of intent: past, present, AND planned
|
|
339
|
+
- When work completes: mark "done"
|
|
340
|
+
- When new objectives emerge: ADD new milestones (don't remove old ones)
|
|
341
|
+
- When the user pivots away from something incomplete: mark it "on_hold" (might return)
|
|
342
|
+
- When an approach is abandoned for a better one: mark it "superseded" (won't return)
|
|
343
|
+
- Available statuses (use exactly these strings): "pending", "in_progress", "done", "on_hold", "superseded"
|
|
344
|
+
- Keep 5-7 visible milestones. Oldest completed items can be dropped if needed to make room.
|
|
345
|
+
|
|
346
|
+
7. NOTABLE - Cumulative event log with rich emojis:
|
|
347
|
+
|
|
348
|
+
Each event has:
|
|
349
|
+
- icon: Expressive emoji matching event type (๐ก ๐ ๐ง โจ ๐ฏ โ
โ ๏ธ ๐ ๐จ ๐ ๐งช ๐ etc.)
|
|
350
|
+
- type: "insight" | "pivot" | "breakthrough" | "infrastructure" | "refinement" | "validation" | "error" | "fix" | "blocker" | "decision" | "iteration"
|
|
351
|
+
- text: What happened (1 sentence)
|
|
352
|
+
|
|
353
|
+
NOTABLE PHILOSOPHY:
|
|
354
|
+
- Events ACCUMULATE over time - don't remove old events
|
|
355
|
+
- New events get added to the list as they occur
|
|
356
|
+
- Use diverse, expressive emojis to make scanning easy
|
|
357
|
+
|
|
358
|
+
8. RECENT ACTIONS - 3-4 most recent concrete actions
|
|
359
|
+
|
|
360
|
+
CRITICAL OUTPUT INSTRUCTIONS:
|
|
361
|
+
- Respond with ONLY valid JSON
|
|
362
|
+
- Do NOT include explanatory text before or after the JSON
|
|
363
|
+
- Do NOT include markdown code blocks or formatting
|
|
364
|
+
- Start your response with { and end with }
|
|
365
|
+
- No prose, no commentary, ONLY the JSON object
|
|
366
|
+
|
|
367
|
+
JSON Structure:
|
|
368
|
+
{
|
|
369
|
+
"context": { "project": "string", "area": "string|null", "mission": "string", "scope": "minor|feature|major" },
|
|
370
|
+
"currentState": { "icon": "emoji (required)", "color": "sky|emerald|amber|red|...", "content": "sentence or bullet points" },
|
|
371
|
+
"theme": "string",
|
|
372
|
+
"tags": { "workType": [...], "domain": [...], "collaboration": "...", "complexity": "..." },
|
|
373
|
+
"panels": [ { "label": "string", "icon": "emoji", "color": "sky|purple|rose|...", "value": "string" } ],
|
|
374
|
+
"milestones": [ { "label": "string", "status": "pending|in_progress|done|on_hold|superseded" } ],
|
|
375
|
+
"notable": [ { "icon": "emoji", "type": "...", "text": "string" } ],
|
|
376
|
+
"recentActions": ["string", ...]
|
|
377
|
+
}`;
|
|
378
|
+
let responseText = null;
|
|
379
|
+
const startTime = Date.now();
|
|
380
|
+
try {
|
|
381
|
+
const response = await this.withRetry('extractSessionInsights', () => this.client.messages.create({
|
|
382
|
+
model: this.models.generation,
|
|
383
|
+
max_tokens: 2000,
|
|
384
|
+
system: systemPrompt,
|
|
385
|
+
messages: [
|
|
386
|
+
{
|
|
387
|
+
role: 'user',
|
|
388
|
+
content: `Analyze this coding session and extract structured insights:\n\n${conversationText}`
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
role: 'assistant',
|
|
392
|
+
content: '{' // Prefill to force JSON output
|
|
393
|
+
}
|
|
394
|
+
]
|
|
395
|
+
}));
|
|
396
|
+
const durationMs = Date.now() - startTime;
|
|
397
|
+
// Log cost for GENERATE operation
|
|
398
|
+
this.logCost(response, 'GENERATE', this.models.generation, durationMs);
|
|
399
|
+
responseText = response.content[0]?.type === 'text' ? response.content[0].text : null;
|
|
400
|
+
if (!responseText) {
|
|
401
|
+
throw new CUIError('ANTHROPIC_INSIGHTS_ERROR', 'No response text returned', 500);
|
|
402
|
+
}
|
|
403
|
+
// Prepend the '{' we used for prefill
|
|
404
|
+
responseText = '{' + responseText;
|
|
405
|
+
// Extract JSON from response with multiple fallback strategies
|
|
406
|
+
let jsonText = responseText.trim();
|
|
407
|
+
// Strategy 1: Try to extract from markdown code blocks
|
|
408
|
+
const codeBlockMatch = responseText.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
409
|
+
if (codeBlockMatch) {
|
|
410
|
+
jsonText = codeBlockMatch[1].trim();
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// Strategy 2: Look for content between first { and last }
|
|
414
|
+
const firstBrace = jsonText.indexOf('{');
|
|
415
|
+
const lastBrace = jsonText.lastIndexOf('}');
|
|
416
|
+
if (firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace) {
|
|
417
|
+
jsonText = jsonText.substring(firstBrace, lastBrace + 1);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// Log the extracted JSON for debugging
|
|
421
|
+
this.logger.debug('Extracted JSON from LLM response', {
|
|
422
|
+
responsePreview: responseText.slice(0, 100),
|
|
423
|
+
extractedPreview: jsonText.slice(0, 100),
|
|
424
|
+
usedCodeBlock: !!codeBlockMatch
|
|
425
|
+
});
|
|
426
|
+
const result = JSON.parse(jsonText);
|
|
427
|
+
// Validate structure - be lenient, provide defaults
|
|
428
|
+
if (!result.context || !result.tags) {
|
|
429
|
+
throw new CUIError('ANTHROPIC_INSIGHTS_ERROR', 'Invalid response structure', 500);
|
|
430
|
+
}
|
|
431
|
+
// Ensure arrays exist with defaults
|
|
432
|
+
result.milestones = result.milestones || [];
|
|
433
|
+
result.notable = result.notable || [];
|
|
434
|
+
result.recentActions = result.recentActions || [];
|
|
435
|
+
result.panels = result.panels || [];
|
|
436
|
+
result.theme = result.theme || 'working';
|
|
437
|
+
result.currentState = result.currentState || { icon: '๐', color: 'sky', content: 'Session in progress' };
|
|
438
|
+
this.logger.info('Session insights extracted (Opus)', {
|
|
439
|
+
model: this.models.generation,
|
|
440
|
+
inputTokens: response.usage?.input_tokens,
|
|
441
|
+
outputTokens: response.usage?.output_tokens,
|
|
442
|
+
project: result.context.project,
|
|
443
|
+
theme: result.theme,
|
|
444
|
+
currentState: result.currentState.content?.slice(0, 50),
|
|
445
|
+
panelCount: result.panels.length,
|
|
446
|
+
panelLabels: result.panels.map(p => p.label || p.title || 'untitled'),
|
|
447
|
+
panelValues: result.panels.map(p => p.value ? `${p.value.slice(0, 50)}...` : p.items ? `${p.items.length} items` : 'empty'),
|
|
448
|
+
milestoneCount: result.milestones.length,
|
|
449
|
+
notableCount: result.notable.length
|
|
450
|
+
});
|
|
451
|
+
return result;
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
if (error instanceof CUIError) {
|
|
455
|
+
throw error;
|
|
456
|
+
}
|
|
457
|
+
// Check for credit/billing errors and make them LOUD
|
|
458
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
459
|
+
if (errorMessage.includes('credit balance') || errorMessage.includes('billing')) {
|
|
460
|
+
this.logger.error('๐จ ANTHROPIC API CREDITS EXHAUSTED ๐จ', {
|
|
461
|
+
errorMessage,
|
|
462
|
+
action: 'ADD CREDITS AT https://console.anthropic.com/settings/billing'
|
|
463
|
+
});
|
|
464
|
+
throw new CUIError('ANTHROPIC_NO_CREDITS', '๐จ Anthropic API credits exhausted. Add credits at https://console.anthropic.com/settings/billing', 402);
|
|
465
|
+
}
|
|
466
|
+
// Log error with proper serialization AND the response that failed to parse
|
|
467
|
+
this.logger.error('Session insights extraction failed', {
|
|
468
|
+
errorMessage,
|
|
469
|
+
errorStack: error instanceof Error ? error.stack : undefined,
|
|
470
|
+
errorType: error?.constructor?.name,
|
|
471
|
+
// Include the actual response for debugging
|
|
472
|
+
responsePreview: responseText?.slice(0, 500) || 'no response'
|
|
473
|
+
});
|
|
474
|
+
throw new CUIError('ANTHROPIC_INSIGHTS_ERROR', 'Failed to extract session insights', 500);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Generate a session name (simpler extraction for naming)
|
|
479
|
+
*/
|
|
480
|
+
async generateSessionName(conversationText) {
|
|
481
|
+
if (!this.client) {
|
|
482
|
+
throw new CUIError('ANTHROPIC_API_KEY_MISSING', 'Anthropic API key not configured', 400);
|
|
483
|
+
}
|
|
484
|
+
try {
|
|
485
|
+
const response = await this.withRetry('generateSessionName', () => this.client.messages.create({
|
|
486
|
+
model: this.models.generation,
|
|
487
|
+
max_tokens: 100,
|
|
488
|
+
messages: [{
|
|
489
|
+
role: 'user',
|
|
490
|
+
content: `Generate a brief, descriptive name (3-6 words) for this coding session. Include the project/area context. Focus on the main task or feature being worked on. Do not include generic words like "session", "conversation", or "chat".
|
|
491
|
+
|
|
492
|
+
Examples of good names:
|
|
493
|
+
- "CUI: Session insights UI redesign"
|
|
494
|
+
- "Auth: Fix redirect loop bug"
|
|
495
|
+
- "API: Add rate limiting middleware"
|
|
496
|
+
|
|
497
|
+
Respond with ONLY the name, nothing else.
|
|
498
|
+
|
|
499
|
+
Conversation:
|
|
500
|
+
${conversationText}`
|
|
501
|
+
}]
|
|
502
|
+
}));
|
|
503
|
+
const name = response.content[0]?.type === 'text' ? response.content[0].text.trim() : null;
|
|
504
|
+
if (!name) {
|
|
505
|
+
throw new CUIError('ANTHROPIC_NAME_ERROR', 'No name returned', 500);
|
|
506
|
+
}
|
|
507
|
+
return { name };
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
if (error instanceof CUIError) {
|
|
511
|
+
throw error;
|
|
512
|
+
}
|
|
513
|
+
this.logger.error('Session name generation failed', { error });
|
|
514
|
+
throw new CUIError('ANTHROPIC_NAME_ERROR', 'Failed to generate session name', 500);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
isConfigured() {
|
|
518
|
+
return this.client !== null;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Quick check using Haiku to determine if insights need patching.
|
|
522
|
+
*
|
|
523
|
+
* This is a fast, cheap call that just answers "has anything meaningful changed?"
|
|
524
|
+
* based on recent actions. If yes, we'll follow up with Sonnet for actual patching.
|
|
525
|
+
*/
|
|
526
|
+
async quickCheckInsightsStale(currentInsights, recentActions) {
|
|
527
|
+
if (!this.client) {
|
|
528
|
+
return { needsPatch: false, reason: 'Anthropic not configured' };
|
|
529
|
+
}
|
|
530
|
+
const startTime = Date.now();
|
|
531
|
+
try {
|
|
532
|
+
const milestonesStr = currentInsights.milestones
|
|
533
|
+
.map(m => `[${m.status}] ${m.label}`)
|
|
534
|
+
.join(', ');
|
|
535
|
+
const panelsStr = currentInsights.panels
|
|
536
|
+
.map(p => p.title)
|
|
537
|
+
.join(', ');
|
|
538
|
+
const actionsStr = recentActions.join('\n');
|
|
539
|
+
const prompt = `Current session insights (a living summary of the conversation):
|
|
540
|
+
- Mission: "${currentInsights.mission}"
|
|
541
|
+
- Milestones: ${milestonesStr || '(none)'}
|
|
542
|
+
- Panels: ${panelsStr || '(none)'}
|
|
543
|
+
|
|
544
|
+
Recent activity:
|
|
545
|
+
${actionsStr}
|
|
546
|
+
|
|
547
|
+
Should these insights be updated to reflect the recent activity?
|
|
548
|
+
|
|
549
|
+
Say YES (needsPatch: true) if ANY of these apply:
|
|
550
|
+
- Work was done that should be reflected in a panel (new file, code change, finding, etc.)
|
|
551
|
+
- Progress was made on any task or milestone
|
|
552
|
+
- Focus shifted to a new area or topic
|
|
553
|
+
- Something notable happened worth capturing
|
|
554
|
+
- The summary feels stale compared to what just happened
|
|
555
|
+
|
|
556
|
+
Say NO only if the recent actions are trivial or already reflected in current insights.
|
|
557
|
+
|
|
558
|
+
Err on the side of updating - the insights should feel LIVE and current.
|
|
559
|
+
|
|
560
|
+
Respond with JSON only:
|
|
561
|
+
{"needsPatch": true/false, "reason": "brief explanation"}`;
|
|
562
|
+
const response = await this.withRetry('quickCheckInsightsStale', () => this.client.messages.create({
|
|
563
|
+
model: this.models.quickCheck,
|
|
564
|
+
max_tokens: 100,
|
|
565
|
+
messages: [{ role: 'user', content: prompt }]
|
|
566
|
+
}));
|
|
567
|
+
const duration = Date.now() - startTime;
|
|
568
|
+
const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
569
|
+
// Log cost for QUICK_CHECK operation
|
|
570
|
+
this.logCost(response, 'QUICK_CHECK', this.models.quickCheck, duration);
|
|
571
|
+
// Log for observability
|
|
572
|
+
this.logger.info('Quick check (Haiku) completed', {
|
|
573
|
+
durationMs: duration,
|
|
574
|
+
inputTokens: response.usage?.input_tokens,
|
|
575
|
+
outputTokens: response.usage?.output_tokens,
|
|
576
|
+
recentActionsCount: recentActions.length,
|
|
577
|
+
responsePreview: text.slice(0, 100)
|
|
578
|
+
});
|
|
579
|
+
try {
|
|
580
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
581
|
+
const result = JSON.parse(jsonMatch?.[0] || text);
|
|
582
|
+
return {
|
|
583
|
+
needsPatch: result.needsPatch === true,
|
|
584
|
+
reason: result.reason || 'No reason provided'
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
catch {
|
|
588
|
+
this.logger.debug('Failed to parse quick check response', { text });
|
|
589
|
+
return { needsPatch: false, reason: 'Parse error - keeping cached' };
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (error) {
|
|
593
|
+
// Check for credit/billing errors and make them LOUD
|
|
594
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
595
|
+
if (errorMessage.includes('credit balance') || errorMessage.includes('billing')) {
|
|
596
|
+
this.logger.error('๐จ ANTHROPIC API CREDITS EXHAUSTED ๐จ', {
|
|
597
|
+
errorMessage,
|
|
598
|
+
action: 'ADD CREDITS AT https://console.anthropic.com/settings/billing',
|
|
599
|
+
context: 'Quick check failed due to billing issue'
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
this.logger.error('Quick check failed', { error });
|
|
604
|
+
}
|
|
605
|
+
return { needsPatch: false, reason: 'Check error - keeping cached' };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Decompose recent activity into exhaustive propositional facts.
|
|
610
|
+
* Separates "what happened" (facts) from "what does this mean" (interpretation).
|
|
611
|
+
* Returns structured bullet points of all tool uses, user messages, and Claude messages.
|
|
612
|
+
*/
|
|
613
|
+
async decomposeActivityIntoPropositions(previousState, recentActivity) {
|
|
614
|
+
if (!this.client) {
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
const previousStateStr = previousState
|
|
618
|
+
? `${previousState.icon} [${previousState.color}] ${previousState.content}`
|
|
619
|
+
: '(no previous state)';
|
|
620
|
+
const activityStr = recentActivity
|
|
621
|
+
.map(a => `[${a.type}] ${a.content}`)
|
|
622
|
+
.join('\n\n');
|
|
623
|
+
const prompt = `You are analyzing a transcript of a coding session between a user and Claude (an AI assistant).
|
|
624
|
+
|
|
625
|
+
**Previous Current State:**
|
|
626
|
+
${previousStateStr}
|
|
627
|
+
|
|
628
|
+
**Recent Activity:**
|
|
629
|
+
${activityStr}
|
|
630
|
+
|
|
631
|
+
**Your Task:**
|
|
632
|
+
Create an exhaustive list of propositional facts about what happened in this activity.
|
|
633
|
+
Be thorough and explicit - list EVERYTHING that was done and said.
|
|
634
|
+
|
|
635
|
+
Format as bullet points:
|
|
636
|
+
- Tool uses: "Claude used [tool] to [action]"
|
|
637
|
+
- Claude statements: "Claude said/stated/explained [fact]"
|
|
638
|
+
- User messages: "User asked/requested/said [fact]"
|
|
639
|
+
|
|
640
|
+
Be exhaustive - every action, every statement, every request should become a bullet point.
|
|
641
|
+
Keep each bullet point factual and specific, not interpretive.
|
|
642
|
+
|
|
643
|
+
Examples:
|
|
644
|
+
โ "Claude used Grep to search for 'validateUser' in the codebase"
|
|
645
|
+
โ "Claude stated that the bug was caused by missing null checks"
|
|
646
|
+
โ "Claude said the fix is ready to test after server restart"
|
|
647
|
+
โ "User asked about the performance impact"
|
|
648
|
+
โ "Claude made progress on the bug" (too vague)
|
|
649
|
+
โ "The session is going well" (interpretive, not factual)
|
|
650
|
+
|
|
651
|
+
Return ONLY a JSON array of strings, one per proposition:
|
|
652
|
+
["proposition 1", "proposition 2", ...]`;
|
|
653
|
+
let rawResponse = '';
|
|
654
|
+
const startTime = Date.now();
|
|
655
|
+
try {
|
|
656
|
+
const response = await this.withRetry('decomposeActivityIntoPropositions', () => this.client.messages.create({
|
|
657
|
+
model: this.models.propositions, // Use Haiku - faster for factual extraction
|
|
658
|
+
max_tokens: 2000,
|
|
659
|
+
temperature: 0,
|
|
660
|
+
messages: [{
|
|
661
|
+
role: 'user',
|
|
662
|
+
content: prompt
|
|
663
|
+
}]
|
|
664
|
+
}));
|
|
665
|
+
const durationMs = Date.now() - startTime;
|
|
666
|
+
// Log cost for PROPOSITIONS operation (Haiku)
|
|
667
|
+
this.logCost(response, 'PROPOSITIONS', this.models.propositions, durationMs);
|
|
668
|
+
const textContent = response.content.find(block => block.type === 'text');
|
|
669
|
+
if (!textContent || textContent.type !== 'text') {
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
672
|
+
rawResponse = textContent.text;
|
|
673
|
+
let jsonText = textContent.text.trim();
|
|
674
|
+
// Strip markdown code blocks if present (```json ... ```)
|
|
675
|
+
if (jsonText.startsWith('```')) {
|
|
676
|
+
jsonText = jsonText.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
|
|
677
|
+
}
|
|
678
|
+
// Strip markdown headings and prose before JSON (e.g., "# Summary\nHere are...")
|
|
679
|
+
// Always try to extract JSON array first, even if text looks like it starts with [
|
|
680
|
+
const jsonMatch = jsonText.match(/\[[\s\S]*?\](?=\s*$)/);
|
|
681
|
+
if (jsonMatch) {
|
|
682
|
+
jsonText = jsonMatch[0];
|
|
683
|
+
}
|
|
684
|
+
else if (!jsonText.startsWith('[')) {
|
|
685
|
+
// No JSON array found and doesn't start with [
|
|
686
|
+
this.logger.debug('Decomposition response is not JSON', { preview: jsonText.slice(0, 200) });
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
const propositions = JSON.parse(jsonText);
|
|
690
|
+
// Log token usage for cost tracking
|
|
691
|
+
this.logger.info('Propositional decomposition (Haiku)', {
|
|
692
|
+
model: this.models.propositions,
|
|
693
|
+
inputTokens: response.usage?.input_tokens,
|
|
694
|
+
outputTokens: response.usage?.output_tokens,
|
|
695
|
+
propositionCount: Array.isArray(propositions) ? propositions.length : 0,
|
|
696
|
+
activityCount: recentActivity.length
|
|
697
|
+
});
|
|
698
|
+
return Array.isArray(propositions) ? propositions : [];
|
|
699
|
+
}
|
|
700
|
+
catch (error) {
|
|
701
|
+
this.logger.debug('Failed to decompose activity into propositions', {
|
|
702
|
+
error: error instanceof Error ? error.message : String(error),
|
|
703
|
+
responsePreview: rawResponse.slice(0, 200),
|
|
704
|
+
});
|
|
705
|
+
return [];
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Fast currentState-only update using Haiku.
|
|
710
|
+
* Bypasses full patch generation when only currentState needs updating.
|
|
711
|
+
* Much faster (~3-4s) than full patch pipeline (~12s).
|
|
712
|
+
*/
|
|
713
|
+
async generateCurrentStateOnly(currentState, propositions) {
|
|
714
|
+
if (!this.client || propositions.length === 0) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
const previousStateStr = currentState
|
|
718
|
+
? `${currentState.icon} [${currentState.color}] ${currentState.content}`
|
|
719
|
+
: '(no previous state)';
|
|
720
|
+
const propositionsStr = propositions.map((p, i) => `${i + 1}. ${p}`).join('\n');
|
|
721
|
+
const prompt = `You are updating the "current state" for a coding session dashboard.
|
|
722
|
+
|
|
723
|
+
**Previous Current State:**
|
|
724
|
+
${previousStateStr}
|
|
725
|
+
|
|
726
|
+
**What Happened Since Then (propositional facts):**
|
|
727
|
+
${propositionsStr}
|
|
728
|
+
|
|
729
|
+
**Your Task:**
|
|
730
|
+
Decide the new current state for the dashboard. This is what someone monitoring the session needs to know RIGHT NOW.
|
|
731
|
+
|
|
732
|
+
${CURRENT_STATE_DECISION_PROMPT}
|
|
733
|
+
|
|
734
|
+
LANGUAGE:
|
|
735
|
+
- Use "we" (session participants working together)
|
|
736
|
+
- NEVER "you"/"your" (dashboard reader is NOT a participant)
|
|
737
|
+
- You're an OBSERVER analyzing their session
|
|
738
|
+
|
|
739
|
+
Respond with ONLY valid JSON:
|
|
740
|
+
{
|
|
741
|
+
"icon": "emoji",
|
|
742
|
+
"color": "amber|red|emerald|sky",
|
|
743
|
+
"content": "single line or bullet points"
|
|
744
|
+
}`;
|
|
745
|
+
let rawResponse = '';
|
|
746
|
+
try {
|
|
747
|
+
const response = await this.withRetry('generateCurrentStateOnly', () => this.client.messages.create({
|
|
748
|
+
model: this.models.patch, // Use Haiku for fast currentState-only updates
|
|
749
|
+
max_tokens: 300,
|
|
750
|
+
temperature: 0,
|
|
751
|
+
messages: [{
|
|
752
|
+
role: 'user',
|
|
753
|
+
content: prompt
|
|
754
|
+
}]
|
|
755
|
+
}));
|
|
756
|
+
const textContent = response.content.find(block => block.type === 'text');
|
|
757
|
+
if (!textContent || textContent.type !== 'text') {
|
|
758
|
+
return null;
|
|
759
|
+
}
|
|
760
|
+
rawResponse = textContent.text;
|
|
761
|
+
let jsonText = textContent.text.trim();
|
|
762
|
+
// Strip markdown code blocks if present (```json ... ```)
|
|
763
|
+
if (jsonText.startsWith('```')) {
|
|
764
|
+
jsonText = jsonText.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
|
|
765
|
+
}
|
|
766
|
+
// Strip markdown headings and prose before JSON (e.g., "# Summary\nHere is...")
|
|
767
|
+
// Always try to extract JSON object first, even if text looks like it starts with {
|
|
768
|
+
const jsonMatch = jsonText.match(/\{[\s\S]*?\}(?=\s*$)/);
|
|
769
|
+
if (jsonMatch) {
|
|
770
|
+
jsonText = jsonMatch[0];
|
|
771
|
+
}
|
|
772
|
+
else if (!jsonText.startsWith('{')) {
|
|
773
|
+
// No JSON object found and doesn't start with {
|
|
774
|
+
this.logger.debug('CurrentState response is not JSON', { preview: jsonText.slice(0, 200) });
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
const result = JSON.parse(jsonText);
|
|
778
|
+
return result;
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
this.logger.debug('Failed to generate currentState', {
|
|
782
|
+
error: error instanceof Error ? error.message : String(error),
|
|
783
|
+
responsePreview: rawResponse.slice(0, 200),
|
|
784
|
+
});
|
|
785
|
+
return null;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Generate a fast patch using Haiku that updates:
|
|
790
|
+
* - currentState
|
|
791
|
+
* - milestones (status updates + new ones)
|
|
792
|
+
* - recentActions
|
|
793
|
+
*
|
|
794
|
+
* This is the new "Haiku-heavy" patch strategy from Architecture V2.
|
|
795
|
+
* Much faster (~1-2s) than full Sonnet patch (~12s).
|
|
796
|
+
* Panels, notable, and context are handled by REFRESH (Opus).
|
|
797
|
+
*/
|
|
798
|
+
async generateFastPatch(currentInsights, propositions) {
|
|
799
|
+
if (!this.client || propositions.length === 0) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
const previousStateStr = currentInsights.currentState
|
|
803
|
+
? `${currentInsights.currentState.icon} [${currentInsights.currentState.color}] ${currentInsights.currentState.content}`
|
|
804
|
+
: '(no previous state)';
|
|
805
|
+
const milestonesStr = currentInsights.milestones.length > 0
|
|
806
|
+
? currentInsights.milestones.map(m => `[${m.status}] ${m.label}`).join('\n')
|
|
807
|
+
: '(none)';
|
|
808
|
+
const propositionsStr = propositions.map((p, i) => `${i + 1}. ${p}`).join('\n');
|
|
809
|
+
const prompt = `You are updating a coding session dashboard based on recent activity.
|
|
810
|
+
|
|
811
|
+
CURRENT STATE:
|
|
812
|
+
${previousStateStr}
|
|
813
|
+
|
|
814
|
+
CURRENT MILESTONES (Progress):
|
|
815
|
+
${milestonesStr}
|
|
816
|
+
|
|
817
|
+
RECENT ACTIVITY (propositional facts):
|
|
818
|
+
${propositionsStr}
|
|
819
|
+
|
|
820
|
+
Update the dashboard to reflect what happened. Return JSON with:
|
|
821
|
+
|
|
822
|
+
1. **currentState**: What's the situation right now?
|
|
823
|
+
- icon: emoji
|
|
824
|
+
- color: amber (waiting), red (blocked), emerald (done), sky (working)
|
|
825
|
+
- content: single sentence or bullet points
|
|
826
|
+
|
|
827
|
+
2. **milestones**: Update statuses AND add forward-looking milestones
|
|
828
|
+
- Mark completed work as "done"
|
|
829
|
+
- Mark active work as "current" (only ONE at a time)
|
|
830
|
+
- IMPORTANT: Add pending milestones for what logically comes NEXT
|
|
831
|
+
- Think ahead: what steps remain to complete the session's goal?
|
|
832
|
+
- Keep labels stable when possible (don't rephrase existing milestones)
|
|
833
|
+
- Use {"action": "update", "label": "exact existing label", "status": "done|current|pending"}
|
|
834
|
+
- Use {"action": "add", "label": "New milestone", "status": "pending"} for upcoming steps
|
|
835
|
+
|
|
836
|
+
3. **recentActions**: 3-4 most recent concrete actions (tool names, file edits, etc.)
|
|
837
|
+
- Keep these short: "Read config.ts", "Edit auth.ts", "Ran tests"
|
|
838
|
+
- Most recent last
|
|
839
|
+
|
|
840
|
+
LANGUAGE:
|
|
841
|
+
- Use "we" (session participants)
|
|
842
|
+
- Never "you/your" (observer, not participant)
|
|
843
|
+
|
|
844
|
+
Respond with ONLY valid JSON:
|
|
845
|
+
{
|
|
846
|
+
"currentState": { "icon": "...", "color": "...", "content": "..." },
|
|
847
|
+
"milestones": [
|
|
848
|
+
{ "action": "update", "label": "exact label", "status": "done" },
|
|
849
|
+
{ "action": "add", "label": "New objective", "status": "pending" }
|
|
850
|
+
],
|
|
851
|
+
"recentActions": ["Action 1", "Action 2", "Action 3"]
|
|
852
|
+
}`;
|
|
853
|
+
let rawResponse = '';
|
|
854
|
+
const startTime = Date.now();
|
|
855
|
+
try {
|
|
856
|
+
const response = await this.withRetry('generateFastPatch', () => this.client.messages.create({
|
|
857
|
+
model: this.models.patch,
|
|
858
|
+
max_tokens: 600,
|
|
859
|
+
temperature: 0,
|
|
860
|
+
messages: [{
|
|
861
|
+
role: 'user',
|
|
862
|
+
content: prompt
|
|
863
|
+
}]
|
|
864
|
+
}));
|
|
865
|
+
const durationMs = Date.now() - startTime;
|
|
866
|
+
// Log cost for FAST_PATCH operation (Haiku)
|
|
867
|
+
this.logCost(response, 'FAST_PATCH', this.models.patch, durationMs);
|
|
868
|
+
const textContent = response.content.find(block => block.type === 'text');
|
|
869
|
+
if (!textContent || textContent.type !== 'text') {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
rawResponse = textContent.text;
|
|
873
|
+
let jsonText = textContent.text.trim();
|
|
874
|
+
// Strip markdown code blocks if present
|
|
875
|
+
if (jsonText.startsWith('```')) {
|
|
876
|
+
jsonText = jsonText.replace(/^```(?:json)?\n?/, '').replace(/\n?```$/, '').trim();
|
|
877
|
+
}
|
|
878
|
+
// Extract JSON object
|
|
879
|
+
const jsonMatch = jsonText.match(/\{[\s\S]*\}(?=\s*$)/);
|
|
880
|
+
if (jsonMatch) {
|
|
881
|
+
jsonText = jsonMatch[0];
|
|
882
|
+
}
|
|
883
|
+
else if (!jsonText.startsWith('{')) {
|
|
884
|
+
this.logger.debug('FastPatch response is not JSON', { preview: jsonText.slice(0, 200) });
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
const result = JSON.parse(jsonText);
|
|
888
|
+
// Validate structure
|
|
889
|
+
if (!result.currentState || !Array.isArray(result.milestones) || !Array.isArray(result.recentActions)) {
|
|
890
|
+
this.logger.debug('FastPatch invalid response structure', { keys: Object.keys(result) });
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
catch (error) {
|
|
896
|
+
this.logger.debug('FastPatch generation failed', {
|
|
897
|
+
error: error instanceof Error ? error.message : String(error),
|
|
898
|
+
responsePreview: rawResponse.slice(0, 200),
|
|
899
|
+
});
|
|
900
|
+
return null;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Generate targeted patches to insights using Sonnet.
|
|
905
|
+
*
|
|
906
|
+
* Called when quickCheckInsightsStale returns needsPatch=true.
|
|
907
|
+
* Returns specific patches to apply rather than regenerating everything.
|
|
908
|
+
*
|
|
909
|
+
* @param useFastPath - If true, only generate currentState using fast Haiku-only pipeline (~5s vs ~12s)
|
|
910
|
+
*/
|
|
911
|
+
async generateInsightsPatch(currentInsights, recentActivity, useFastPath = true // Default to fast path for better UX
|
|
912
|
+
) {
|
|
913
|
+
if (!this.client) {
|
|
914
|
+
return { patches: {}, reason: 'Anthropic not configured', propositions: [] };
|
|
915
|
+
}
|
|
916
|
+
const startTime = Date.now();
|
|
917
|
+
try {
|
|
918
|
+
// Step 1: Decompose activity into propositional facts
|
|
919
|
+
const decompositionStart = Date.now();
|
|
920
|
+
const propositions = await this.decomposeActivityIntoPropositions(currentInsights.currentState, recentActivity);
|
|
921
|
+
const decompositionTime = Date.now() - decompositionStart;
|
|
922
|
+
this.logger.debug('Propositional decomposition completed', {
|
|
923
|
+
durationMs: decompositionTime,
|
|
924
|
+
propositionCount: propositions.length,
|
|
925
|
+
});
|
|
926
|
+
// Fast path: Generate currentState + milestones + recentActions using Haiku (~1-2s vs ~12s full patch)
|
|
927
|
+
// This is the "Haiku-heavy" strategy from Architecture V2
|
|
928
|
+
if (useFastPath) {
|
|
929
|
+
this.logger.debug('Using fast path for patch generation');
|
|
930
|
+
const fastPatch = await this.generateFastPatch({
|
|
931
|
+
currentState: currentInsights.currentState,
|
|
932
|
+
milestones: currentInsights.milestones,
|
|
933
|
+
recentActions: currentInsights.recentActions,
|
|
934
|
+
}, propositions);
|
|
935
|
+
const totalTime = Date.now() - startTime;
|
|
936
|
+
if (fastPatch) {
|
|
937
|
+
const patches = {};
|
|
938
|
+
// Always include currentState if present
|
|
939
|
+
if (fastPatch.currentState) {
|
|
940
|
+
patches.currentState = fastPatch.currentState;
|
|
941
|
+
}
|
|
942
|
+
// Include milestones if there are any changes
|
|
943
|
+
if (fastPatch.milestones && fastPatch.milestones.length > 0) {
|
|
944
|
+
patches.milestones = fastPatch.milestones;
|
|
945
|
+
}
|
|
946
|
+
// Include recentActions if present
|
|
947
|
+
if (fastPatch.recentActions && fastPatch.recentActions.length > 0) {
|
|
948
|
+
patches.recentActions = fastPatch.recentActions;
|
|
949
|
+
}
|
|
950
|
+
const patchedFields = Object.keys(patches);
|
|
951
|
+
this.logger.info('Fast path patch completed', {
|
|
952
|
+
durationMs: totalTime,
|
|
953
|
+
patchedFields,
|
|
954
|
+
});
|
|
955
|
+
return {
|
|
956
|
+
patches,
|
|
957
|
+
reason: `Fast path: Updated ${patchedFields.join(', ')} based on propositional analysis`,
|
|
958
|
+
propositions
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
// Fall through to full path if fast path fails
|
|
962
|
+
this.logger.debug('Fast path returned null, falling back to full patch generation');
|
|
963
|
+
}
|
|
964
|
+
const milestonesStr = currentInsights.milestones
|
|
965
|
+
.map((m, i) => ` ${i}: [${m.status}] ${m.label}`)
|
|
966
|
+
.join('\n');
|
|
967
|
+
const panelsStr = currentInsights.panels
|
|
968
|
+
.map(p => {
|
|
969
|
+
// Handle both V8 format (label/value) and old format (title/items)
|
|
970
|
+
const panelLabel = p.label || p.title || 'Untitled';
|
|
971
|
+
const panelContent = p.value
|
|
972
|
+
? p.value // Show full value, not truncated
|
|
973
|
+
: p.items?.map(item => typeof item === 'string' ? item : item.text).join(', ') || '(empty)'; // Show all items, not just first 3
|
|
974
|
+
return ` - "${panelLabel}": ${panelContent}`;
|
|
975
|
+
})
|
|
976
|
+
.join('\n');
|
|
977
|
+
// Use propositions if available, fallback to raw activity
|
|
978
|
+
const activityStr = propositions.length > 0
|
|
979
|
+
? propositions.map(p => `โข ${p}`).join('\n')
|
|
980
|
+
: recentActivity.slice(-10).map(a => `[${a.type}] ${a.content}`).join('\n');
|
|
981
|
+
const currentStateStr = currentInsights.currentState
|
|
982
|
+
? `${currentInsights.currentState.icon} [${currentInsights.currentState.color}] ${currentInsights.currentState.content}`
|
|
983
|
+
: '(none)';
|
|
984
|
+
// Format previous propositions if available
|
|
985
|
+
const previousPropsStr = (currentInsights.previousPropositions && currentInsights.previousPropositions.length > 0)
|
|
986
|
+
? currentInsights.previousPropositions.map(p => `โข ${p}`).join('\n')
|
|
987
|
+
: null;
|
|
988
|
+
const prompt = `You are analyzing a coding session transcript to update a dashboard display.
|
|
989
|
+
|
|
990
|
+
IMPORTANT CONTEXT:
|
|
991
|
+
- You are an OBSERVER looking at a transcript of a human ("the user") working with Claude ("the assistant")
|
|
992
|
+
- The "RECENT ACTIVITY" below shows what happened in THEIR session
|
|
993
|
+
- Your job is to update the dashboard to reflect what's happening in their session
|
|
994
|
+
- NEVER ask questions or request clarification - just analyze what happened and update accordingly
|
|
995
|
+
- The "current state" should describe THEIR session state, not ask meta-questions about the insights
|
|
996
|
+
|
|
997
|
+
CURRENT SESSION INSIGHTS:
|
|
998
|
+
Mission: "${currentInsights.mission}"
|
|
999
|
+
Theme: ${currentInsights.theme}
|
|
1000
|
+
Current State: ${currentStateStr}
|
|
1001
|
+
|
|
1002
|
+
Milestones:
|
|
1003
|
+
${milestonesStr || ' (none)'}
|
|
1004
|
+
|
|
1005
|
+
Panels:
|
|
1006
|
+
${panelsStr || ' (none)'}
|
|
1007
|
+
|
|
1008
|
+
Notable events:
|
|
1009
|
+
${currentInsights.notable.map(n => ` โข ${n.icon || 'โข'} ${n.text}`).join('\n') || ' (none)'}
|
|
1010
|
+
|
|
1011
|
+
${previousPropsStr ? `PROPOSITIONS THAT GENERATED CURRENT INSIGHTS:
|
|
1012
|
+
${previousPropsStr}
|
|
1013
|
+
|
|
1014
|
+
` : ''}WHAT HAPPENED SINCE LAST UPDATE (exhaustive propositional facts):
|
|
1015
|
+
${activityStr}
|
|
1016
|
+
|
|
1017
|
+
The above is an exhaustive, factual breakdown of everything that was done and said since the last current state update.
|
|
1018
|
+
Each bullet point is a specific, verifiable fact extracted from the session transcript.
|
|
1019
|
+
|
|
1020
|
+
Generate PATCHES to update these insights based on what happened.
|
|
1021
|
+
Only include fields that need changing. Be surgical - don't regenerate what's still accurate.
|
|
1022
|
+
|
|
1023
|
+
PANEL RULES (STRICT - NEVER VIOLATE):
|
|
1024
|
+
- Sessions have EXACTLY 4 panels - this is a hard limit
|
|
1025
|
+
- CURRENT PANELS: ${currentInsights.panels.map(p => `"${p.label || p.title}"`).join(', ')}
|
|
1026
|
+
- To change a panel's content: use {"action": "update", "label": "exact label from above", "value": "new content"}
|
|
1027
|
+
- To completely replace a panel: use {"action": "remove", "label": "old label"} then {"action": "add", "label": "New Label", ...}
|
|
1028
|
+
- You can ONLY use "add" if there are fewer than 4 panels total after removals
|
|
1029
|
+
- When in doubt, UPDATE existing panels rather than adding new ones
|
|
1030
|
+
- Match panel labels EXACTLY as shown above
|
|
1031
|
+
|
|
1032
|
+
NOTABLE EVENT GUIDELINES:
|
|
1033
|
+
- Only add truly SIGNIFICANT events: breakthroughs, blockers, major decisions, errors, fixes, pivots, insights
|
|
1034
|
+
- Do NOT add routine actions like "added logging" or "updated tests"
|
|
1035
|
+
- Each event should be meaningful enough that someone reviewing the session would want to know about it
|
|
1036
|
+
- Use specific event types: "insight", "pivot", "breakthrough", "infrastructure", "refinement", "validation", "error", "fix", "blocker", "decision", "iteration", "correction"
|
|
1037
|
+
- Choose expressive emojis that capture the meaning: ๐ก๐๐งโจ๐ฏโ
โ ๏ธ๐๐จ๐๐งช๐๐ฅโก๐ ๏ธ๐๏ธ
|
|
1038
|
+
- Keep text to one concise sentence describing what happened and why it matters
|
|
1039
|
+
|
|
1040
|
+
CURRENT STATE - ALWAYS UPDATE THIS (HIGHEST PRIORITY):
|
|
1041
|
+
|
|
1042
|
+
${CURRENT_STATE_DECISION_PROMPT}
|
|
1043
|
+
|
|
1044
|
+
Response format (JSON only):
|
|
1045
|
+
{
|
|
1046
|
+
"reason": "brief explanation of changes",
|
|
1047
|
+
"patches": {
|
|
1048
|
+
"mission": "new mission if changed",
|
|
1049
|
+
"description": "new description if changed",
|
|
1050
|
+
"theme": "new theme if changed",
|
|
1051
|
+
"currentState": {"icon": "emoji", "color": "sky|emerald|amber|red|...", "content": "short description of current state"},
|
|
1052
|
+
"milestones": [
|
|
1053
|
+
{"action": "update", "label": "exact label", "status": "done|in_progress|pending|on_hold|superseded"},
|
|
1054
|
+
{"action": "add", "label": "new milestone", "status": "pending"}
|
|
1055
|
+
],
|
|
1056
|
+
"panels": [
|
|
1057
|
+
{"action": "update", "label": "exact label", "value": "updated single sentence"},
|
|
1058
|
+
{"action": "add", "label": "New Panel", "icon": "emoji", "color": "sky|purple|rose|orange|emerald|amber|red|teal|blue|fuchsia|slate", "value": "one sentence"},
|
|
1059
|
+
{"action": "remove", "label": "Obsolete Panel"}
|
|
1060
|
+
],
|
|
1061
|
+
"notable": [
|
|
1062
|
+
{"action": "add", "type": "breakthrough|insight|fix|error|blocker|decision|pivot|iteration|infrastructure|refinement|validation|correction", "text": "What happened and why it matters", "icon": "๐"}
|
|
1063
|
+
],
|
|
1064
|
+
"recentActions": ["Action 1", "Action 2"],
|
|
1065
|
+
"tags": ["tag1", "tag2"]
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
Only include patches object keys that need updating. Omit unchanged fields (except currentState which should usually be updated).`;
|
|
1070
|
+
const patchGenStart = Date.now();
|
|
1071
|
+
const response = await this.withRetry('generateInsightsPatch', () => this.client.messages.create({
|
|
1072
|
+
model: this.models.fullPatch,
|
|
1073
|
+
max_tokens: 800,
|
|
1074
|
+
temperature: 0, // Lower temperature for more consistent currentState decisions
|
|
1075
|
+
messages: [{ role: 'user', content: prompt }]
|
|
1076
|
+
}));
|
|
1077
|
+
const patchGenTime = Date.now() - patchGenStart;
|
|
1078
|
+
const duration = Date.now() - startTime;
|
|
1079
|
+
const text = response.content[0]?.type === 'text' ? response.content[0].text : '';
|
|
1080
|
+
// Log cost for PATCH operation (full path)
|
|
1081
|
+
this.logCost(response, 'PATCH', this.models.fullPatch, duration);
|
|
1082
|
+
// Log for observability
|
|
1083
|
+
this.logger.info('Patch generation (Sonnet) completed', {
|
|
1084
|
+
durationMs: duration,
|
|
1085
|
+
inputTokens: response.usage?.input_tokens,
|
|
1086
|
+
outputTokens: response.usage?.output_tokens,
|
|
1087
|
+
activityCount: recentActivity.length,
|
|
1088
|
+
responseLength: text.length
|
|
1089
|
+
});
|
|
1090
|
+
try {
|
|
1091
|
+
const jsonMatch = text.match(/\{[\s\S]*\}/);
|
|
1092
|
+
const result = JSON.parse(jsonMatch?.[0] || text);
|
|
1093
|
+
const patches = result.patches || {};
|
|
1094
|
+
// Log what's being patched for observability
|
|
1095
|
+
const patchedFields = Object.keys(patches).filter(k => patches[k] !== undefined);
|
|
1096
|
+
this.logger.info('Patch fields identified', {
|
|
1097
|
+
patchedFields,
|
|
1098
|
+
milestonesPatched: patches.milestones?.length || 0,
|
|
1099
|
+
panelsPatched: patches.panels?.length || 0,
|
|
1100
|
+
notableAdded: patches.notable?.length || 0
|
|
1101
|
+
});
|
|
1102
|
+
return {
|
|
1103
|
+
patches,
|
|
1104
|
+
reason: result.reason || 'Patches generated',
|
|
1105
|
+
propositions
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
catch {
|
|
1109
|
+
this.logger.debug('Failed to parse patch response', { text });
|
|
1110
|
+
return { patches: {}, reason: 'Parse error - no patches applied', propositions };
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
catch (error) {
|
|
1114
|
+
// Check for credit/billing errors and make them LOUD
|
|
1115
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1116
|
+
if (errorMessage.includes('credit balance') || errorMessage.includes('billing')) {
|
|
1117
|
+
this.logger.error('๐จ ANTHROPIC API CREDITS EXHAUSTED ๐จ', {
|
|
1118
|
+
errorMessage,
|
|
1119
|
+
action: 'ADD CREDITS AT https://console.anthropic.com/settings/billing',
|
|
1120
|
+
context: 'Patch generation failed due to billing issue'
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
else {
|
|
1124
|
+
this.logger.error('Patch generation failed', { error });
|
|
1125
|
+
}
|
|
1126
|
+
return { patches: {}, reason: 'Generation error - no patches applied', propositions: [] };
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
// Export singleton instance
|
|
1131
|
+
export const anthropicService = new AnthropicService();
|
|
1132
|
+
//# sourceMappingURL=anthropic-service.js.map
|