brownian-code 2026.2.10
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 +21 -0
- package/README.md +97 -0
- package/bin/brownian +25 -0
- package/env.example +21 -0
- package/package.json +87 -0
- package/src/agent/agent.test.ts +414 -0
- package/src/agent/agent.ts +385 -0
- package/src/agent/index.ts +27 -0
- package/src/agent/prompts.ts +271 -0
- package/src/agent/scratchpad.test.ts +482 -0
- package/src/agent/scratchpad.ts +526 -0
- package/src/agent/token-counter.test.ts +59 -0
- package/src/agent/token-counter.ts +33 -0
- package/src/agent/types.ts +137 -0
- package/src/cli.tsx +385 -0
- package/src/commands/builtin.test.ts +271 -0
- package/src/commands/builtin.ts +200 -0
- package/src/commands/registry.test.ts +188 -0
- package/src/commands/registry.ts +111 -0
- package/src/commands/types.ts +64 -0
- package/src/components/AgentEventView.tsx +487 -0
- package/src/components/AnswerBox.tsx +81 -0
- package/src/components/ApiKeyPrompt.tsx +75 -0
- package/src/components/CommandMenu.test.tsx +64 -0
- package/src/components/CommandMenu.tsx +38 -0
- package/src/components/CursorText.tsx +43 -0
- package/src/components/DebugPanel.tsx +48 -0
- package/src/components/ErrorBox.test.tsx +58 -0
- package/src/components/ErrorBox.tsx +26 -0
- package/src/components/HelpView.test.tsx +70 -0
- package/src/components/HelpView.tsx +61 -0
- package/src/components/HistoryItemView.tsx +108 -0
- package/src/components/Input.tsx +193 -0
- package/src/components/Intro.test.tsx +59 -0
- package/src/components/Intro.tsx +35 -0
- package/src/components/ModelSelector.tsx +288 -0
- package/src/components/StatusBar.test.tsx +78 -0
- package/src/components/StatusBar.tsx +56 -0
- package/src/components/WorkingIndicator.tsx +133 -0
- package/src/components/index.ts +23 -0
- package/src/e2e/agent-flow.test.ts +378 -0
- package/src/evals/components/EvalApp.tsx +206 -0
- package/src/evals/components/EvalCurrentQuestion.tsx +42 -0
- package/src/evals/components/EvalProgress.tsx +33 -0
- package/src/evals/components/EvalRecentResults.tsx +63 -0
- package/src/evals/components/EvalStats.tsx +49 -0
- package/src/evals/components/index.ts +5 -0
- package/src/evals/dataset/crypto_agent.csv +16 -0
- package/src/evals/run.ts +355 -0
- package/src/gateway/channels/whatsapp/auth-store.ts +15 -0
- package/src/gateway/channels/whatsapp/inbound.ts +86 -0
- package/src/gateway/channels/whatsapp/login.ts +28 -0
- package/src/gateway/channels/whatsapp/outbound.ts +27 -0
- package/src/gateway/channels/whatsapp/session.ts +69 -0
- package/src/gateway/config.ts +81 -0
- package/src/gateway/index.ts +62 -0
- package/src/hooks/useAgentRunner.ts +317 -0
- package/src/hooks/useDebugLogs.ts +22 -0
- package/src/hooks/useInputHistory.ts +106 -0
- package/src/hooks/useModelSelection.ts +249 -0
- package/src/hooks/useTextBuffer.test.ts +121 -0
- package/src/hooks/useTextBuffer.ts +97 -0
- package/src/index.tsx +74 -0
- package/src/mcp/cache.ts +205 -0
- package/src/mcp/client.test.ts +126 -0
- package/src/mcp/client.ts +145 -0
- package/src/mcp/index.ts +2 -0
- package/src/model/llm.test.ts +158 -0
- package/src/model/llm.ts +233 -0
- package/src/providers.ts +94 -0
- package/src/skills/index.ts +17 -0
- package/src/skills/loader.ts +73 -0
- package/src/skills/registry.ts +125 -0
- package/src/skills/types.ts +31 -0
- package/src/test-utils/mocks.ts +110 -0
- package/src/theme.ts +21 -0
- package/src/tools/browser/browser.ts +357 -0
- package/src/tools/browser/index.ts +1 -0
- package/src/tools/crypto/hive-tools.ts +171 -0
- package/src/tools/crypto/index.ts +1 -0
- package/src/tools/descriptions/browser.ts +105 -0
- package/src/tools/descriptions/crypto-search.ts +58 -0
- package/src/tools/descriptions/index.ts +8 -0
- package/src/tools/descriptions/web-fetch.ts +44 -0
- package/src/tools/descriptions/web-search.ts +26 -0
- package/src/tools/fetch/cache.ts +95 -0
- package/src/tools/fetch/external-content.ts +200 -0
- package/src/tools/fetch/index.ts +1 -0
- package/src/tools/fetch/web-fetch-utils.ts +122 -0
- package/src/tools/fetch/web-fetch.ts +371 -0
- package/src/tools/index.ts +12 -0
- package/src/tools/registry.ts +130 -0
- package/src/tools/search/exa.ts +43 -0
- package/src/tools/search/index.ts +2 -0
- package/src/tools/search/tavily.ts +35 -0
- package/src/tools/skill.ts +62 -0
- package/src/tools/types.ts +53 -0
- package/src/utils/ai-message.ts +26 -0
- package/src/utils/config.ts +54 -0
- package/src/utils/cost-calculator.test.ts +101 -0
- package/src/utils/cost-calculator.ts +74 -0
- package/src/utils/env.ts +101 -0
- package/src/utils/error-classifier.test.ts +146 -0
- package/src/utils/error-classifier.ts +91 -0
- package/src/utils/in-memory-chat-history.test.ts +291 -0
- package/src/utils/in-memory-chat-history.ts +224 -0
- package/src/utils/index.ts +19 -0
- package/src/utils/input-key-handlers.test.ts +155 -0
- package/src/utils/input-key-handlers.ts +64 -0
- package/src/utils/logger.ts +67 -0
- package/src/utils/long-term-chat-history.ts +138 -0
- package/src/utils/markdown-table.ts +227 -0
- package/src/utils/ollama.ts +37 -0
- package/src/utils/progress-channel.ts +84 -0
- package/src/utils/text-navigation.test.ts +222 -0
- package/src/utils/text-navigation.ts +81 -0
- package/src/utils/thinking-verbs.ts +29 -0
- package/src/utils/tokens.test.ts +163 -0
- package/src/utils/tokens.ts +67 -0
- package/src/utils/tool-description.ts +88 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Brownian WhatsApp Gateway — Entry Point
|
|
4
|
+
*
|
|
5
|
+
* Run: bun run gateway
|
|
6
|
+
*
|
|
7
|
+
* Listens for self-messages on WhatsApp, routes them to the Brownian agent,
|
|
8
|
+
* and sends responses back. Handles graceful shutdown on SIGINT/SIGTERM.
|
|
9
|
+
*/
|
|
10
|
+
import { config } from 'dotenv';
|
|
11
|
+
import { loadGatewayConfig } from './config.js';
|
|
12
|
+
import { createWhatsAppSession } from './channels/whatsapp/session.js';
|
|
13
|
+
import { registerInboundHandler } from './channels/whatsapp/inbound.js';
|
|
14
|
+
import { disconnectHiveMCP } from '../mcp/client.js';
|
|
15
|
+
import type { WASocket } from '@whiskeysockets/baileys';
|
|
16
|
+
|
|
17
|
+
// Load environment variables
|
|
18
|
+
config({ quiet: true });
|
|
19
|
+
|
|
20
|
+
const gatewayConfig = loadGatewayConfig();
|
|
21
|
+
let activeSock: WASocket | null = null;
|
|
22
|
+
|
|
23
|
+
console.log('Brownian WhatsApp Gateway');
|
|
24
|
+
console.log('Connecting to WhatsApp...\n');
|
|
25
|
+
|
|
26
|
+
if (!gatewayConfig.channels.whatsapp.enabled) {
|
|
27
|
+
console.error('WhatsApp channel is disabled in ~/.brownian/gateway.json');
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await createWhatsAppSession({
|
|
32
|
+
printQR: true,
|
|
33
|
+
onReady: (sock) => {
|
|
34
|
+
activeSock = sock;
|
|
35
|
+
registerInboundHandler(sock, gatewayConfig.channels.whatsapp);
|
|
36
|
+
console.log('Gateway is running. Send yourself a WhatsApp message to query Brownian.');
|
|
37
|
+
console.log('Press Ctrl+C to stop.\n');
|
|
38
|
+
},
|
|
39
|
+
onLoggedOut: () => {
|
|
40
|
+
console.log('Session logged out. Run: bun run gateway:login');
|
|
41
|
+
process.exit(1);
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Graceful shutdown
|
|
46
|
+
async function shutdown() {
|
|
47
|
+
console.log('\nShutting down gateway...');
|
|
48
|
+
|
|
49
|
+
if (activeSock) {
|
|
50
|
+
try {
|
|
51
|
+
await activeSock.logout();
|
|
52
|
+
} catch {
|
|
53
|
+
// Best-effort
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
await disconnectHiveMCP();
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
process.on('SIGINT', shutdown);
|
|
62
|
+
process.on('SIGTERM', shutdown);
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { useState, useCallback, useRef } from 'react';
|
|
2
|
+
import { Agent } from '../agent/agent.js';
|
|
3
|
+
import { InMemoryChatHistory } from '../utils/in-memory-chat-history.js';
|
|
4
|
+
import { estimateCost } from '../utils/cost-calculator.js';
|
|
5
|
+
import { callLlm, getFastModel } from '../model/llm.js';
|
|
6
|
+
import type { HistoryItem, WorkingState } from '../components/index.js';
|
|
7
|
+
import type { AgentConfig, AgentEvent, DoneEvent } from '../agent/index.js';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Types
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export interface RunQueryResult {
|
|
14
|
+
answer: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface UseAgentRunnerResult {
|
|
18
|
+
// State
|
|
19
|
+
history: HistoryItem[];
|
|
20
|
+
workingState: WorkingState;
|
|
21
|
+
error: string | null;
|
|
22
|
+
isProcessing: boolean;
|
|
23
|
+
|
|
24
|
+
// Cumulative session stats
|
|
25
|
+
cumulativeTokens: number;
|
|
26
|
+
cumulativeCost: number;
|
|
27
|
+
turnCount: number;
|
|
28
|
+
|
|
29
|
+
// Actions
|
|
30
|
+
runQuery: (query: string) => Promise<RunQueryResult | undefined>;
|
|
31
|
+
runCompactQuery: (instructions?: string) => Promise<void>;
|
|
32
|
+
cancelExecution: () => void;
|
|
33
|
+
setError: (error: string | null) => void;
|
|
34
|
+
clearHistory: () => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Hook
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
export function useAgentRunner(
|
|
42
|
+
agentConfig: AgentConfig,
|
|
43
|
+
inMemoryChatHistoryRef: React.RefObject<InMemoryChatHistory>
|
|
44
|
+
): UseAgentRunnerResult {
|
|
45
|
+
const [history, setHistory] = useState<HistoryItem[]>([]);
|
|
46
|
+
const [workingState, setWorkingState] = useState<WorkingState>({ status: 'idle' });
|
|
47
|
+
const [error, setError] = useState<string | null>(null);
|
|
48
|
+
|
|
49
|
+
// Cumulative session stats
|
|
50
|
+
const [cumulativeTokens, setCumulativeTokens] = useState(0);
|
|
51
|
+
const [cumulativeCost, setCumulativeCost] = useState(0);
|
|
52
|
+
const [turnCount, setTurnCount] = useState(0);
|
|
53
|
+
|
|
54
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
55
|
+
|
|
56
|
+
// Helper to update the last (processing) history item
|
|
57
|
+
const updateLastHistoryItem = useCallback((
|
|
58
|
+
updater: (item: HistoryItem) => Partial<HistoryItem>
|
|
59
|
+
) => {
|
|
60
|
+
setHistory(prev => {
|
|
61
|
+
const last = prev[prev.length - 1];
|
|
62
|
+
if (!last || last.status !== 'processing') return prev;
|
|
63
|
+
return [...prev.slice(0, -1), { ...last, ...updater(last) }];
|
|
64
|
+
});
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
// Handle agent events
|
|
68
|
+
const handleEvent = useCallback((event: AgentEvent) => {
|
|
69
|
+
switch (event.type) {
|
|
70
|
+
case 'thinking':
|
|
71
|
+
setWorkingState({ status: 'thinking' });
|
|
72
|
+
updateLastHistoryItem(item => ({
|
|
73
|
+
events: [...item.events, {
|
|
74
|
+
id: `thinking-${Date.now()}`,
|
|
75
|
+
event,
|
|
76
|
+
completed: true,
|
|
77
|
+
}],
|
|
78
|
+
}));
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'tool_start': {
|
|
82
|
+
const toolId = `tool-${event.tool}-${Date.now()}`;
|
|
83
|
+
setWorkingState({ status: 'tool', toolName: event.tool });
|
|
84
|
+
updateLastHistoryItem(item => ({
|
|
85
|
+
activeToolId: toolId,
|
|
86
|
+
events: [...item.events, {
|
|
87
|
+
id: toolId,
|
|
88
|
+
event,
|
|
89
|
+
completed: false,
|
|
90
|
+
}],
|
|
91
|
+
}));
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'tool_progress':
|
|
96
|
+
updateLastHistoryItem(item => ({
|
|
97
|
+
events: item.events.map(e =>
|
|
98
|
+
e.id === item.activeToolId
|
|
99
|
+
? { ...e, progressMessage: event.message }
|
|
100
|
+
: e
|
|
101
|
+
),
|
|
102
|
+
}));
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'tool_end':
|
|
106
|
+
setWorkingState({ status: 'thinking' });
|
|
107
|
+
updateLastHistoryItem(item => ({
|
|
108
|
+
activeToolId: undefined,
|
|
109
|
+
events: item.events.map(e =>
|
|
110
|
+
e.id === item.activeToolId
|
|
111
|
+
? { ...e, completed: true, endEvent: event }
|
|
112
|
+
: e
|
|
113
|
+
),
|
|
114
|
+
}));
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
case 'tool_error':
|
|
118
|
+
setWorkingState({ status: 'thinking' });
|
|
119
|
+
updateLastHistoryItem(item => ({
|
|
120
|
+
activeToolId: undefined,
|
|
121
|
+
events: item.events.map(e =>
|
|
122
|
+
e.id === item.activeToolId
|
|
123
|
+
? { ...e, completed: true, endEvent: event }
|
|
124
|
+
: e
|
|
125
|
+
),
|
|
126
|
+
}));
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case 'answer_start':
|
|
130
|
+
setWorkingState({ status: 'answering', startTime: Date.now() });
|
|
131
|
+
break;
|
|
132
|
+
|
|
133
|
+
case 'done': {
|
|
134
|
+
const doneEvent = event as DoneEvent;
|
|
135
|
+
|
|
136
|
+
// Accumulate session stats
|
|
137
|
+
if (doneEvent.tokenUsage) {
|
|
138
|
+
setCumulativeTokens(prev => prev + doneEvent.tokenUsage!.totalTokens);
|
|
139
|
+
const cost = estimateCost(
|
|
140
|
+
agentConfig.model || '',
|
|
141
|
+
doneEvent.tokenUsage.inputTokens,
|
|
142
|
+
doneEvent.tokenUsage.outputTokens
|
|
143
|
+
);
|
|
144
|
+
setCumulativeCost(prev => prev + cost.totalCost);
|
|
145
|
+
}
|
|
146
|
+
setTurnCount(prev => prev + 1);
|
|
147
|
+
|
|
148
|
+
updateLastHistoryItem(item => {
|
|
149
|
+
// Update answer in chat history for multi-turn context
|
|
150
|
+
if (doneEvent.answer) {
|
|
151
|
+
inMemoryChatHistoryRef.current?.saveAnswer(doneEvent.answer).catch(() => {
|
|
152
|
+
// Silently ignore errors in updating history
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
answer: doneEvent.answer,
|
|
157
|
+
status: 'complete' as const,
|
|
158
|
+
duration: doneEvent.totalTime,
|
|
159
|
+
tokenUsage: doneEvent.tokenUsage,
|
|
160
|
+
tokensPerSecond: doneEvent.tokensPerSecond,
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
setWorkingState({ status: 'idle' });
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}, [updateLastHistoryItem, inMemoryChatHistoryRef, agentConfig.model]);
|
|
168
|
+
|
|
169
|
+
// Run a query through the agent
|
|
170
|
+
const runQuery = useCallback(async (query: string): Promise<RunQueryResult | undefined> => {
|
|
171
|
+
// Create abort controller for this execution
|
|
172
|
+
const abortController = new AbortController();
|
|
173
|
+
abortControllerRef.current = abortController;
|
|
174
|
+
|
|
175
|
+
// Track the final answer to return
|
|
176
|
+
let finalAnswer: string | undefined;
|
|
177
|
+
|
|
178
|
+
// Add to history immediately
|
|
179
|
+
const itemId = Date.now().toString();
|
|
180
|
+
const startTime = Date.now();
|
|
181
|
+
setHistory(prev => [...prev, {
|
|
182
|
+
id: itemId,
|
|
183
|
+
query,
|
|
184
|
+
events: [],
|
|
185
|
+
answer: '',
|
|
186
|
+
status: 'processing',
|
|
187
|
+
startTime,
|
|
188
|
+
}]);
|
|
189
|
+
|
|
190
|
+
// Save query to chat history immediately for multi-turn context
|
|
191
|
+
inMemoryChatHistoryRef.current?.saveUserQuery(query);
|
|
192
|
+
|
|
193
|
+
setError(null);
|
|
194
|
+
setWorkingState({ status: 'thinking' });
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const agent = await Agent.create({
|
|
198
|
+
...agentConfig,
|
|
199
|
+
signal: abortController.signal,
|
|
200
|
+
});
|
|
201
|
+
const stream = agent.run(query, inMemoryChatHistoryRef.current!);
|
|
202
|
+
|
|
203
|
+
for await (const event of stream) {
|
|
204
|
+
// Capture the final answer from the done event
|
|
205
|
+
if (event.type === 'done') {
|
|
206
|
+
finalAnswer = (event as DoneEvent).answer;
|
|
207
|
+
}
|
|
208
|
+
handleEvent(event);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Return the answer if we got one
|
|
212
|
+
if (finalAnswer) {
|
|
213
|
+
return { answer: finalAnswer };
|
|
214
|
+
}
|
|
215
|
+
} catch (e) {
|
|
216
|
+
// Handle abort gracefully - mark as interrupted, not error
|
|
217
|
+
if (e instanceof Error && e.name === 'AbortError') {
|
|
218
|
+
setHistory(prev => {
|
|
219
|
+
const last = prev[prev.length - 1];
|
|
220
|
+
if (!last || last.status !== 'processing') return prev;
|
|
221
|
+
return [...prev.slice(0, -1), { ...last, status: 'interrupted' }];
|
|
222
|
+
});
|
|
223
|
+
setWorkingState({ status: 'idle' });
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const errorMsg = e instanceof Error ? e.message : String(e);
|
|
228
|
+
setError(errorMsg);
|
|
229
|
+
// Mark the history item as error
|
|
230
|
+
setHistory(prev => {
|
|
231
|
+
const last = prev[prev.length - 1];
|
|
232
|
+
if (!last || last.status !== 'processing') return prev;
|
|
233
|
+
return [...prev.slice(0, -1), { ...last, status: 'error' }];
|
|
234
|
+
});
|
|
235
|
+
setWorkingState({ status: 'idle' });
|
|
236
|
+
return undefined;
|
|
237
|
+
} finally {
|
|
238
|
+
abortControllerRef.current = null;
|
|
239
|
+
}
|
|
240
|
+
}, [agentConfig, inMemoryChatHistoryRef, handleEvent]);
|
|
241
|
+
|
|
242
|
+
// Cancel the current execution
|
|
243
|
+
const cancelExecution = useCallback(() => {
|
|
244
|
+
if (abortControllerRef.current) {
|
|
245
|
+
abortControllerRef.current.abort();
|
|
246
|
+
abortControllerRef.current = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Mark current processing item as interrupted
|
|
250
|
+
setHistory(prev => {
|
|
251
|
+
const last = prev[prev.length - 1];
|
|
252
|
+
if (!last || last.status !== 'processing') return prev;
|
|
253
|
+
return [...prev.slice(0, -1), { ...last, status: 'interrupted' }];
|
|
254
|
+
});
|
|
255
|
+
setWorkingState({ status: 'idle' });
|
|
256
|
+
}, []);
|
|
257
|
+
|
|
258
|
+
// Clear conversation history (session only)
|
|
259
|
+
const clearHistory = useCallback(() => {
|
|
260
|
+
setHistory([]);
|
|
261
|
+
setError(null);
|
|
262
|
+
setTurnCount(0);
|
|
263
|
+
setCumulativeTokens(0);
|
|
264
|
+
setCumulativeCost(0);
|
|
265
|
+
// Clear in-memory chat context but keep persistent history
|
|
266
|
+
inMemoryChatHistoryRef.current?.clear();
|
|
267
|
+
}, [inMemoryChatHistoryRef]);
|
|
268
|
+
|
|
269
|
+
// Compact conversation: summarize history, clear, and reset counters
|
|
270
|
+
const runCompactQuery = useCallback(async (instructions?: string) => {
|
|
271
|
+
const chatHistory = inMemoryChatHistoryRef.current;
|
|
272
|
+
if (!chatHistory?.hasMessages()) {
|
|
273
|
+
setError('Nothing to compact.');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const messages = chatHistory.getMessages();
|
|
278
|
+
const summaryPrompt = `Summarize this conversation concisely:\n${messages.map(m =>
|
|
279
|
+
`User: ${m.query}\nAssistant: ${m.summary || m.answer || '(pending)'}`
|
|
280
|
+
).join('\n\n')}${instructions ? `\nFocus on: ${instructions}` : ''}`;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const fastModel = getFastModel(agentConfig.modelProvider || 'anthropic', agentConfig.model || '');
|
|
284
|
+
const { response } = await callLlm(summaryPrompt, {
|
|
285
|
+
model: fastModel,
|
|
286
|
+
systemPrompt: 'Summarize the conversation briefly. Keep key facts, numbers, and decisions.',
|
|
287
|
+
});
|
|
288
|
+
const summary = typeof response === 'string' ? response : String(response);
|
|
289
|
+
|
|
290
|
+
chatHistory.clear();
|
|
291
|
+
chatHistory.saveUserQuery('[Compacted conversation]');
|
|
292
|
+
await chatHistory.saveAnswer(summary);
|
|
293
|
+
setCumulativeTokens(0);
|
|
294
|
+
setCumulativeCost(0);
|
|
295
|
+
} catch (e) {
|
|
296
|
+
setError(`Compact failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
297
|
+
}
|
|
298
|
+
}, [inMemoryChatHistoryRef, agentConfig.modelProvider, agentConfig.model, setError]);
|
|
299
|
+
|
|
300
|
+
// Check if currently processing
|
|
301
|
+
const isProcessing = history.length > 0 && history[history.length - 1].status === 'processing';
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
history,
|
|
305
|
+
workingState,
|
|
306
|
+
error,
|
|
307
|
+
isProcessing,
|
|
308
|
+
cumulativeTokens,
|
|
309
|
+
cumulativeCost,
|
|
310
|
+
turnCount,
|
|
311
|
+
runQuery,
|
|
312
|
+
runCompactQuery,
|
|
313
|
+
cancelExecution,
|
|
314
|
+
setError,
|
|
315
|
+
clearHistory,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import type { LogEntry } from '../utils/logger.js';
|
|
3
|
+
import { logger } from '../utils/logger.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to get debug logs for display in the UI.
|
|
7
|
+
*/
|
|
8
|
+
export function useDebugLogs(): LogEntry[] {
|
|
9
|
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
10
|
+
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const unsubscribe = logger.subscribe((entries) => {
|
|
13
|
+
setLogs(entries);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return () => {
|
|
17
|
+
unsubscribe();
|
|
18
|
+
};
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return logs;
|
|
22
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { LongTermChatHistory } from '../utils/long-term-chat-history.js';
|
|
3
|
+
|
|
4
|
+
export interface UseInputHistoryResult {
|
|
5
|
+
/** Current history value to display (null = user is typing fresh input) */
|
|
6
|
+
historyValue: string | null;
|
|
7
|
+
/** Navigate to older message (up arrow) */
|
|
8
|
+
navigateUp: () => void;
|
|
9
|
+
/** Navigate to newer message (down arrow) */
|
|
10
|
+
navigateDown: () => void;
|
|
11
|
+
/** Save a user message to history */
|
|
12
|
+
saveMessage: (message: string) => Promise<void>;
|
|
13
|
+
/** Update the agent response for the most recent conversation */
|
|
14
|
+
updateAgentResponse: (response: string) => Promise<void>;
|
|
15
|
+
/** Reset navigation back to typing mode */
|
|
16
|
+
resetNavigation: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook for managing input history navigation.
|
|
21
|
+
* Allows users to scroll through previous messages using up/down arrows.
|
|
22
|
+
*
|
|
23
|
+
* Uses stack ordering (newest first) for O(1) access:
|
|
24
|
+
* - historyIndex = -1: User is typing (not navigating history)
|
|
25
|
+
* - historyIndex = 0: Most recent message
|
|
26
|
+
* - historyIndex = N: N messages back from most recent
|
|
27
|
+
*/
|
|
28
|
+
export function useInputHistory(): UseInputHistoryResult {
|
|
29
|
+
const storeRef = useRef<LongTermChatHistory>(new LongTermChatHistory());
|
|
30
|
+
const [messages, setMessages] = useState<string[]>([]);
|
|
31
|
+
const [historyIndex, setHistoryIndex] = useState<number>(-1);
|
|
32
|
+
|
|
33
|
+
// Load messages on mount
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
const loadMessages = async () => {
|
|
36
|
+
await storeRef.current.load();
|
|
37
|
+
setMessages(storeRef.current.getMessageStrings());
|
|
38
|
+
};
|
|
39
|
+
loadMessages();
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
// Navigate to older message (up arrow)
|
|
43
|
+
const navigateUp = useCallback(() => {
|
|
44
|
+
if (messages.length === 0) return;
|
|
45
|
+
|
|
46
|
+
setHistoryIndex(prev => {
|
|
47
|
+
const maxIndex = messages.length - 1;
|
|
48
|
+
if (prev === -1) {
|
|
49
|
+
// Start navigation from most recent
|
|
50
|
+
return 0;
|
|
51
|
+
} else if (prev < maxIndex) {
|
|
52
|
+
// Move to older message
|
|
53
|
+
return prev + 1;
|
|
54
|
+
}
|
|
55
|
+
// At oldest message, stay there
|
|
56
|
+
return prev;
|
|
57
|
+
});
|
|
58
|
+
}, [messages.length]);
|
|
59
|
+
|
|
60
|
+
// Navigate to newer message (down arrow)
|
|
61
|
+
const navigateDown = useCallback(() => {
|
|
62
|
+
setHistoryIndex(prev => {
|
|
63
|
+
if (prev === -1) {
|
|
64
|
+
// Already in typing mode, do nothing
|
|
65
|
+
return -1;
|
|
66
|
+
} else if (prev === 0) {
|
|
67
|
+
// At most recent, go back to typing mode
|
|
68
|
+
return -1;
|
|
69
|
+
} else {
|
|
70
|
+
// Move to newer message
|
|
71
|
+
return prev - 1;
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}, []);
|
|
75
|
+
|
|
76
|
+
// Save a new user message to history
|
|
77
|
+
const saveMessage = useCallback(async (message: string) => {
|
|
78
|
+
await storeRef.current.addUserMessage(message);
|
|
79
|
+
setMessages(storeRef.current.getMessageStrings());
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
// Update agent response for most recent conversation
|
|
83
|
+
const updateAgentResponse = useCallback(async (response: string) => {
|
|
84
|
+
await storeRef.current.updateAgentResponse(response);
|
|
85
|
+
}, []);
|
|
86
|
+
|
|
87
|
+
// Reset navigation to typing mode
|
|
88
|
+
const resetNavigation = useCallback(() => {
|
|
89
|
+
setHistoryIndex(-1);
|
|
90
|
+
}, []);
|
|
91
|
+
|
|
92
|
+
// Compute the current history value based on index
|
|
93
|
+
// Stack ordering: messages[0] is most recent, direct access
|
|
94
|
+
const historyValue = historyIndex === -1
|
|
95
|
+
? null
|
|
96
|
+
: messages[historyIndex] ?? null;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
historyValue,
|
|
100
|
+
navigateUp,
|
|
101
|
+
navigateDown,
|
|
102
|
+
saveMessage,
|
|
103
|
+
updateAgentResponse,
|
|
104
|
+
resetNavigation,
|
|
105
|
+
};
|
|
106
|
+
}
|