mirror of https://github.com/buster-so/buster.git
starting to piece together ui for tasks
This commit is contained in:
parent
a59d4df920
commit
edfa13e785
|
@ -10,34 +10,20 @@ import {
|
|||
ChatVersionTagline,
|
||||
VimStatus,
|
||||
} from '../components/chat-layout';
|
||||
import { Diff } from '../components/diff';
|
||||
import { SettingsForm } from '../components/settings-form';
|
||||
import { type MessageType, TypedMessage } from '../components/typed-message';
|
||||
import { AgentMessageComponent } from '../components/typed-message';
|
||||
import type { DocsAgentMessage } from '../services/analytics-engineer-handler';
|
||||
import { getSetting } from '../utils/settings';
|
||||
import type { SlashCommand } from '../utils/slash-commands';
|
||||
import type { VimMode } from '../utils/vim-mode';
|
||||
|
||||
type AppMode = 'Planning' | 'Auto-accept' | 'None';
|
||||
|
||||
interface Message {
|
||||
id: number;
|
||||
type: 'user' | 'assistant';
|
||||
content: string;
|
||||
messageType?: MessageType;
|
||||
metadata?: string;
|
||||
diffLines?: Array<{
|
||||
lineNumber: number;
|
||||
content: string;
|
||||
type: 'add' | 'remove' | 'context';
|
||||
}>;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export function Main() {
|
||||
const { exit } = useApp();
|
||||
const [input, setInput] = useState('');
|
||||
const [history, setHistory] = useState<ChatHistoryEntry[]>([]);
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [messages, setMessages] = useState<DocsAgentMessage[]>([]);
|
||||
const historyCounter = useRef(0);
|
||||
const messageCounter = useRef(0);
|
||||
const [vimEnabled, setVimEnabled] = useState(() => getSetting('vimMode'));
|
||||
|
@ -66,7 +52,43 @@ export function Main() {
|
|||
}
|
||||
});
|
||||
|
||||
const getMockResponse = (userInput: string): Message[] => {
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
setInput('');
|
||||
return;
|
||||
}
|
||||
|
||||
const userMessage: DocsAgentMessage = {
|
||||
id: ++messageCounter.current,
|
||||
message: {
|
||||
kind: 'user',
|
||||
content: trimmed,
|
||||
},
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput('');
|
||||
|
||||
// Import and run the docs agent
|
||||
const { runDocsAgent } = await import('../services/analytics-engineer-handler');
|
||||
|
||||
// Run agent - callbacks will handle message display
|
||||
await runDocsAgent({
|
||||
userMessage: trimmed,
|
||||
onMessage: (agentMessage) => {
|
||||
// Assign unique ID to each message
|
||||
const messageWithId = {
|
||||
id: ++messageCounter.current,
|
||||
message: agentMessage.message,
|
||||
};
|
||||
setMessages((prev) => [...prev, messageWithId]);
|
||||
},
|
||||
});
|
||||
}, [input]);
|
||||
|
||||
// REMOVED: Old getMockResponse function
|
||||
const _oldGetMockResponse = (userInput: string) => {
|
||||
const responses: Message[] = [];
|
||||
|
||||
if (userInput.toLowerCase().includes('plan')) {
|
||||
|
@ -225,47 +247,9 @@ export function Main() {
|
|||
});
|
||||
}
|
||||
|
||||
return responses;
|
||||
return []; // Old mock code removed
|
||||
};
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
setInput('');
|
||||
return;
|
||||
}
|
||||
|
||||
messageCounter.current += 1;
|
||||
const userMessage: Message = {
|
||||
id: messageCounter.current,
|
||||
type: 'user',
|
||||
content: trimmed,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, userMessage]);
|
||||
setInput('');
|
||||
|
||||
// Import and run the docs agent
|
||||
const { runDocsAgent } = await import('../services/analytics-engineer-handler');
|
||||
|
||||
await runDocsAgent({
|
||||
userMessage: trimmed,
|
||||
onMessage: (agentMessage) => {
|
||||
messageCounter.current += 1;
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: messageCounter.current,
|
||||
type: agentMessage.type,
|
||||
content: agentMessage.content,
|
||||
messageType: agentMessage.messageType,
|
||||
metadata: agentMessage.metadata,
|
||||
},
|
||||
]);
|
||||
},
|
||||
});
|
||||
}, [input]);
|
||||
|
||||
const handleCommandExecute = useCallback(
|
||||
(command: SlashCommand) => {
|
||||
switch (command.action) {
|
||||
|
@ -309,35 +293,9 @@ export function Main() {
|
|||
|
||||
// Memoize message list to prevent re-renders from cursor blinking
|
||||
const messageList = useMemo(() => {
|
||||
return messages.map((message) => {
|
||||
if (message.type === 'user') {
|
||||
return (
|
||||
<Box key={message.id} marginBottom={1}>
|
||||
<Text color="#a855f7" bold>
|
||||
❯{' '}
|
||||
</Text>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
} else if (message.messageType) {
|
||||
return (
|
||||
<Box key={message.id} flexDirection="column">
|
||||
<TypedMessage
|
||||
type={message.messageType}
|
||||
content={message.content}
|
||||
metadata={message.metadata}
|
||||
/>
|
||||
{message.diffLines && <Diff lines={message.diffLines} fileName={message.fileName} />}
|
||||
</Box>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Box key={message.id} marginBottom={1}>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
return messages.map((msg) => (
|
||||
<AgentMessageComponent key={msg.id} message={msg.message} />
|
||||
));
|
||||
}, [messages]);
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
import { Box, Text, useInput } from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
import type { AgentMessage } from '../services/analytics-engineer-handler';
|
||||
|
||||
interface ExecuteMessageProps {
|
||||
message: Extract<AgentMessage, { kind: 'bash' | 'grep' | 'ls' }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying bash, grep, and ls command execution
|
||||
* Shows EXECUTE badge, command description, and output logs
|
||||
* Supports expansion with Ctrl+O to show full output
|
||||
*/
|
||||
export function ExecuteMessage({ message }: ExecuteMessageProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Handle Ctrl+O to toggle expansion
|
||||
useInput((input, key) => {
|
||||
if (key.ctrl && input === 'o') {
|
||||
setIsExpanded((prev) => !prev);
|
||||
}
|
||||
});
|
||||
|
||||
const { args, result } = message;
|
||||
|
||||
// Get command description and output based on tool type
|
||||
let description = '';
|
||||
let output = '';
|
||||
let exitCode: number | undefined;
|
||||
let success = true;
|
||||
|
||||
if (message.kind === 'bash') {
|
||||
description = args.description || args.command;
|
||||
if (result) {
|
||||
output = result.stdout || result.stderr || '';
|
||||
exitCode = result.exitCode;
|
||||
success = result.success;
|
||||
}
|
||||
} else if (message.kind === 'grep') {
|
||||
description = `Search for "${args.pattern}"${args.glob ? ` in ${args.glob}` : ''}`;
|
||||
if (result) {
|
||||
output = result.matches
|
||||
.map((m) => `${m.path}:${m.lineNum}: ${m.lineText}`)
|
||||
.join('\n');
|
||||
success = result.totalMatches > 0;
|
||||
}
|
||||
} else if (message.kind === 'ls') {
|
||||
description = `List directory ${args.path || '.'}`;
|
||||
if (result) {
|
||||
output = result.output;
|
||||
success = result.success;
|
||||
}
|
||||
}
|
||||
|
||||
// Split output into lines for display
|
||||
const outputLines = output.split('\n').filter(Boolean);
|
||||
|
||||
// Show last 5 lines when not expanded, all lines when expanded
|
||||
const displayLines = isExpanded ? outputLines : outputLines.slice(-5);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{/* EXECUTE badge with actual command in parentheses */}
|
||||
<Box flexDirection="row">
|
||||
<Text bold color="white" backgroundColor="orange">
|
||||
EXECUTE
|
||||
</Text>
|
||||
<Text color="#94a3b8"> ({args.command})</Text>
|
||||
</Box>
|
||||
|
||||
{/* Output lines - always show with indentation */}
|
||||
{outputLines.length > 0 && (
|
||||
<Box flexDirection="column" paddingLeft={2}>
|
||||
{displayLines.map((line, idx) => (
|
||||
<Text key={idx} color="#e0e7ff">
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Exit code/status line with indentation */}
|
||||
{message.kind === 'bash' && exitCode !== undefined && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color={success ? '#64748b' : 'red'} dimColor>
|
||||
↳ Exit code: {exitCode}. Output: {outputLines.length} lines.
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{message.kind === 'grep' && result && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color={result.totalMatches > 0 ? 'green' : 'yellow'}>
|
||||
↳ Found {result.totalMatches} match{result.totalMatches !== 1 ? 'es' : ''}
|
||||
{result.truncated ? ' (truncated)' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{message.kind === 'ls' && result && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color={result.success ? 'green' : 'red'}>
|
||||
↳ Listed {result.count} file{result.count !== 1 ? 's' : ''}
|
||||
{result.truncated ? ' (truncated)' : ''}
|
||||
{result.errorMessage ? `: ${result.errorMessage}` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Expansion hint if output is long */}
|
||||
{outputLines.length > 5 && (
|
||||
<Box paddingLeft={2}>
|
||||
<Text color="#64748b" dimColor>
|
||||
{isExpanded ? '(Press Ctrl+O to collapse)' : `... (${outputLines.length - 5} more lines, press Ctrl+O to expand)`}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,35 +1,48 @@
|
|||
import { Box, Text } from 'ink';
|
||||
import React from 'react';
|
||||
import type { AgentMessage } from '../services/analytics-engineer-handler';
|
||||
import { ExecuteMessage } from './execute-message';
|
||||
|
||||
export type MessageType = 'PLAN' | 'EXECUTE' | 'WRITE' | 'EDIT';
|
||||
|
||||
interface TypedMessageProps {
|
||||
type: MessageType;
|
||||
content: string;
|
||||
metadata?: string;
|
||||
interface AgentMessageComponentProps {
|
||||
message: AgentMessage;
|
||||
}
|
||||
|
||||
const typeStyles: Record<MessageType, { bg: string; fg: string; label: string }> = {
|
||||
PLAN: { bg: '#fb923c', fg: '#000000', label: 'PLAN' },
|
||||
EXECUTE: { bg: '#fb923c', fg: '#000000', label: 'EXECUTE' },
|
||||
WRITE: { bg: '#3b82f6', fg: '#ffffff', label: 'WRITE' },
|
||||
EDIT: { bg: '#22c55e', fg: '#ffffff', label: 'EDIT' },
|
||||
};
|
||||
export function AgentMessageComponent({ message }: AgentMessageComponentProps) {
|
||||
switch (message.kind) {
|
||||
case 'user':
|
||||
return (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="#a855f7" bold>
|
||||
❯{' '}
|
||||
</Text>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export function TypedMessage({ type, content, metadata }: TypedMessageProps) {
|
||||
const style = typeStyles[type];
|
||||
case 'text-delta':
|
||||
return (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="#e0e7ff">{message.content}</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Box gap={1}>
|
||||
<Text backgroundColor={style.bg} color={style.fg} bold>
|
||||
{` ${style.label} `}
|
||||
</Text>
|
||||
{metadata && <Text dimColor>{metadata}</Text>}
|
||||
</Box>
|
||||
<Box marginLeft={2} marginTop={0}>
|
||||
<Text>{content}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
case 'idle':
|
||||
// For idle tool, just show the final response as plain text
|
||||
return (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="#e0e7ff">
|
||||
{message.args?.final_response || 'Task completed'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
case 'bash':
|
||||
case 'grep':
|
||||
case 'ls':
|
||||
// For execute commands, use the ExecuteMessage component
|
||||
return <ExecuteMessage message={message} />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,45 @@
|
|||
import { randomUUID } from 'node:crypto';
|
||||
import { createProxyModel } from '@buster/ai/llm/providers/proxy-model';
|
||||
import type { ModelMessage } from '@buster/ai';
|
||||
import type {
|
||||
BashToolInput,
|
||||
BashToolOutput,
|
||||
GrepToolInput,
|
||||
GrepToolOutput,
|
||||
IdleInput,
|
||||
LsToolInput,
|
||||
LsToolOutput,
|
||||
ModelMessage,
|
||||
ToolEvent,
|
||||
} from '@buster/ai';
|
||||
import { getProxyConfig } from '../utils/ai-proxy';
|
||||
import { createAnalyticsEngineerAgent } from '@buster/ai/agents/analytics-engineer-agent/analytics-engineer-agent';
|
||||
|
||||
// Discriminated union for all possible message types - uses tool types directly
|
||||
export type AgentMessage =
|
||||
| { kind: 'user'; content: string }
|
||||
| { kind: 'text-delta'; content: string }
|
||||
| { kind: 'idle'; args: IdleInput }
|
||||
| {
|
||||
kind: 'bash';
|
||||
event: 'start' | 'complete';
|
||||
args: BashToolInput;
|
||||
result?: BashToolOutput;
|
||||
}
|
||||
| {
|
||||
kind: 'grep';
|
||||
event: 'start' | 'complete';
|
||||
args: GrepToolInput;
|
||||
result?: GrepToolOutput;
|
||||
}
|
||||
| {
|
||||
kind: 'ls';
|
||||
event: 'start' | 'complete';
|
||||
args: LsToolInput;
|
||||
result?: LsToolOutput;
|
||||
};
|
||||
|
||||
export interface DocsAgentMessage {
|
||||
id: number;
|
||||
type: 'user' | 'assistant';
|
||||
content: string;
|
||||
messageType?: 'PLAN' | 'EDIT' | 'EXECUTE' | 'WRITE' | 'WEB_SEARCH';
|
||||
metadata?: string;
|
||||
message: AgentMessage;
|
||||
}
|
||||
|
||||
export interface RunDocsAgentParams {
|
||||
|
@ -21,11 +51,9 @@ export interface RunDocsAgentParams {
|
|||
* Runs the docs agent in the CLI without sandbox
|
||||
* The agent runs locally but uses the proxy model to route LLM calls through the server
|
||||
*/
|
||||
export async function runDocsAgent(params: RunDocsAgentParams): Promise<void> {
|
||||
export async function runDocsAgent(params: RunDocsAgentParams) {
|
||||
const { userMessage, onMessage } = params;
|
||||
|
||||
let messageId = 1;
|
||||
|
||||
// Get proxy configuration
|
||||
const proxyConfig = await getProxyConfig();
|
||||
|
||||
|
@ -36,16 +64,65 @@ export async function runDocsAgent(params: RunDocsAgentParams): Promise<void> {
|
|||
modelId: 'anthropic/claude-4-sonnet-20250514',
|
||||
});
|
||||
|
||||
// Create the docs agent with proxy model
|
||||
// Create the docs agent with proxy model and typed event callback
|
||||
// Tools are handled locally, only model calls go through proxy
|
||||
const analyticsEngineerAgent = createAnalyticsEngineerAgent({
|
||||
folder_structure: 'CLI mode - limited file access',
|
||||
folder_structure: process.cwd(), // Use current working directory for CLI mode
|
||||
userId: 'cli-user',
|
||||
chatId: randomUUID(),
|
||||
dataSourceId: '',
|
||||
organizationId: 'cli',
|
||||
messageId: randomUUID(),
|
||||
model: proxyModel,
|
||||
// Handle typed tool events - TypeScript knows exact shape based on discriminants
|
||||
onToolEvent: (event: ToolEvent) => {
|
||||
// Type narrowing: TypeScript knows event.args and event.result types!
|
||||
if (event.tool === 'idleTool' && event.event === 'complete') {
|
||||
// event.args is IdleInput, event.result is IdleOutput - fully typed!
|
||||
onMessage({
|
||||
message: {
|
||||
kind: 'idle',
|
||||
args: event.args, // Type-safe: IdleInput
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle bash tool events - only show complete to avoid duplicates
|
||||
if (event.tool === 'bashTool' && event.event === 'complete') {
|
||||
onMessage({
|
||||
message: {
|
||||
kind: 'bash',
|
||||
event: 'complete',
|
||||
args: event.args,
|
||||
result: event.result, // Type-safe: BashToolOutput
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle grep tool events - only show complete to avoid duplicates
|
||||
if (event.tool === 'grepTool' && event.event === 'complete') {
|
||||
onMessage({
|
||||
message: {
|
||||
kind: 'grep',
|
||||
event: 'complete',
|
||||
args: event.args,
|
||||
result: event.result, // Type-safe: GrepToolOutput
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Handle ls tool events - only show complete to avoid duplicates
|
||||
if (event.tool === 'lsTool' && event.event === 'complete') {
|
||||
onMessage({
|
||||
message: {
|
||||
kind: 'ls',
|
||||
event: 'complete',
|
||||
args: event.args,
|
||||
result: event.result, // Type-safe: LsToolOutput
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const messages: ModelMessage[] = [
|
||||
|
@ -55,65 +132,13 @@ export async function runDocsAgent(params: RunDocsAgentParams): Promise<void> {
|
|||
},
|
||||
];
|
||||
|
||||
try {
|
||||
// Execute the docs agent
|
||||
const result = await analyticsEngineerAgent.stream({ messages });
|
||||
// Start the stream - this triggers the agent to run
|
||||
const stream = await analyticsEngineerAgent.stream({ messages });
|
||||
|
||||
// Stream the response
|
||||
for await (const part of result.fullStream) {
|
||||
// Handle different stream part types
|
||||
if (part.type === 'text-delta') {
|
||||
onMessage({
|
||||
id: messageId++,
|
||||
type: 'assistant',
|
||||
content: part.delta,
|
||||
});
|
||||
} else if (part.type === 'tool-call') {
|
||||
// Map tool calls to message types
|
||||
let messageType: DocsAgentMessage['messageType'];
|
||||
let content = '';
|
||||
const metadata = '';
|
||||
|
||||
switch (part.toolName) {
|
||||
case 'sequentialThinking':
|
||||
messageType = 'PLAN';
|
||||
content = 'Planning next steps...';
|
||||
break;
|
||||
case 'bashExecute':
|
||||
messageType = 'EXECUTE';
|
||||
content = 'Executing command...';
|
||||
break;
|
||||
case 'webSearch':
|
||||
messageType = 'WEB_SEARCH';
|
||||
content = 'Searching the web...';
|
||||
break;
|
||||
case 'grepSearch':
|
||||
messageType = 'EXECUTE';
|
||||
content = 'Searching files...';
|
||||
break;
|
||||
case 'idleTool':
|
||||
messageType = 'EXECUTE';
|
||||
content = 'Entering idle state...';
|
||||
break;
|
||||
default:
|
||||
content = `Using tool: ${part.toolName}`;
|
||||
}
|
||||
|
||||
onMessage({
|
||||
id: messageId++,
|
||||
type: 'assistant',
|
||||
content,
|
||||
messageType,
|
||||
metadata,
|
||||
});
|
||||
}
|
||||
// Ignore other stream part types (start, finish, etc.)
|
||||
}
|
||||
} catch (error) {
|
||||
onMessage({
|
||||
id: messageId++,
|
||||
type: 'assistant',
|
||||
content: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
});
|
||||
// Consume the stream to trigger tool execution
|
||||
// Tools will call callbacks directly when they execute
|
||||
for await (const _part of stream.fullStream) {
|
||||
// Stream parts are consumed but tools handle their own display via callbacks
|
||||
// In the future we could handle text-delta here if needed
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import { createGrepTool } from '../../tools/file-tools/grep-tool/grep-tool';
|
|||
import { createReadFileTool } from '../../tools/file-tools/read-file-tool/read-file-tool';
|
||||
import { type AgentContext, repairToolCall } from '../../utils/tool-call-repair';
|
||||
import { getDocsAgentSystemPrompt as getAnalyticsEngineerAgentSystemPrompt } from './get-analytics-engineer-agent-system-prompt';
|
||||
import type { ToolEventCallback } from './tool-events';
|
||||
|
||||
export const ANALYST_ENGINEER_AGENT_NAME = 'analyticsEngineerAgent';
|
||||
|
||||
|
@ -42,7 +43,9 @@ const AnalyticsEngineerAgentStreamOptionsSchema = z.object({
|
|||
messages: z.array(z.custom<ModelMessage>()).describe('The messages to send to the docs agent'),
|
||||
});
|
||||
|
||||
export type AnalyticsEngineerAgentOptions = z.infer<typeof AnalyticsEngineerAgentOptionsSchema>;
|
||||
export type AnalyticsEngineerAgentOptions = z.infer<typeof AnalyticsEngineerAgentOptionsSchema> & {
|
||||
onToolEvent?: ToolEventCallback;
|
||||
};
|
||||
export type AnalyticsEngineerAgentStreamOptions = z.infer<typeof AnalyticsEngineerAgentStreamOptionsSchema>;
|
||||
|
||||
// Extended type for passing to tools (includes sandbox)
|
||||
|
@ -55,7 +58,9 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
|
|||
providerOptions: DEFAULT_ANTHROPIC_OPTIONS,
|
||||
} as ModelMessage;
|
||||
|
||||
const idleTool = createIdleTool();
|
||||
const idleTool = createIdleTool({
|
||||
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
|
||||
});
|
||||
const writeFileTool = createWriteFileTool({
|
||||
messageId: analyticsEngineerAgentOptions.messageId,
|
||||
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
|
||||
|
@ -63,6 +68,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
|
|||
const grepTool = createGrepTool({
|
||||
messageId: analyticsEngineerAgentOptions.messageId,
|
||||
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
|
||||
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
|
||||
});
|
||||
const readFileTool = createReadFileTool({
|
||||
messageId: analyticsEngineerAgentOptions.messageId,
|
||||
|
@ -71,6 +77,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
|
|||
const bashTool = createBashTool({
|
||||
messageId: analyticsEngineerAgentOptions.messageId,
|
||||
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
|
||||
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
|
||||
});
|
||||
const editFileTool = createEditFileTool({
|
||||
messageId: analyticsEngineerAgentOptions.messageId,
|
||||
|
@ -83,6 +90,7 @@ export function createAnalyticsEngineerAgent(analyticsEngineerAgentOptions: Anal
|
|||
const lsTool = createLsTool({
|
||||
messageId: analyticsEngineerAgentOptions.messageId,
|
||||
projectDirectory: analyticsEngineerAgentOptions.folder_structure,
|
||||
onToolEvent: analyticsEngineerAgentOptions.onToolEvent,
|
||||
});
|
||||
|
||||
// Create planning tools with simple context
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import type { IdleInput, IdleOutput } from '../../tools/communication-tools/idle-tool/idle-tool';
|
||||
import type { BashToolInput, BashToolOutput } from '../../tools/file-tools/bash-tool/bash-tool';
|
||||
import type {
|
||||
EditFileToolInput,
|
||||
EditFileToolOutput,
|
||||
} from '../../tools/file-tools/edit-file-tool/edit-file-tool';
|
||||
import type { GrepToolInput, GrepToolOutput } from '../../tools/file-tools/grep-tool/grep-tool';
|
||||
import type { LsToolInput, LsToolOutput } from '../../tools/file-tools/ls-tool/ls-tool';
|
||||
import type {
|
||||
ReadFileToolInput,
|
||||
ReadFileToolOutput,
|
||||
} from '../../tools/file-tools/read-file-tool/read-file-tool';
|
||||
import type {
|
||||
WriteFileToolInput,
|
||||
WriteFileToolOutput,
|
||||
} from '../../tools/file-tools/write-file-tool/write-file-tool';
|
||||
|
||||
/**
|
||||
* Discriminated union of all possible tool events in the analytics engineer agent
|
||||
* This provides full type safety from tools -> CLI display
|
||||
*/
|
||||
export type ToolEvent =
|
||||
// Idle tool events
|
||||
| { tool: 'idleTool'; event: 'start'; args: IdleInput }
|
||||
| { tool: 'idleTool'; event: 'complete'; result: IdleOutput; args: IdleInput }
|
||||
// Bash tool events
|
||||
| { tool: 'bashTool'; event: 'start'; args: BashToolInput }
|
||||
| { tool: 'bashTool'; event: 'complete'; result: BashToolOutput; args: BashToolInput }
|
||||
// Grep tool events
|
||||
| { tool: 'grepTool'; event: 'start'; args: GrepToolInput }
|
||||
| { tool: 'grepTool'; event: 'complete'; result: GrepToolOutput; args: GrepToolInput }
|
||||
// Read file tool events
|
||||
| { tool: 'readFileTool'; event: 'start'; args: ReadFileToolInput }
|
||||
| { tool: 'readFileTool'; event: 'complete'; result: ReadFileToolOutput; args: ReadFileToolInput }
|
||||
// Write file tool events
|
||||
| { tool: 'writeFileTool'; event: 'start'; args: WriteFileToolInput }
|
||||
| {
|
||||
tool: 'writeFileTool';
|
||||
event: 'complete';
|
||||
result: WriteFileToolOutput;
|
||||
args: WriteFileToolInput;
|
||||
}
|
||||
// Edit file tool events
|
||||
| { tool: 'editFileTool'; event: 'start'; args: EditFileToolInput }
|
||||
| { tool: 'editFileTool'; event: 'complete'; result: EditFileToolOutput; args: EditFileToolInput }
|
||||
// Ls tool events
|
||||
| { tool: 'lsTool'; event: 'start'; args: LsToolInput }
|
||||
| { tool: 'lsTool'; event: 'complete'; result: LsToolOutput; args: LsToolInput };
|
||||
|
||||
/**
|
||||
* Callback type for tool events - single typed callback for all tools
|
||||
*/
|
||||
export type ToolEventCallback = (event: ToolEvent) => void;
|
|
@ -4,3 +4,36 @@ export * from './workflows';
|
|||
export * from './utils';
|
||||
export * from './embeddings';
|
||||
export * from './tasks';
|
||||
|
||||
// Export tool types for CLI usage
|
||||
export type {
|
||||
IdleInput,
|
||||
IdleOutput,
|
||||
} from './tools/communication-tools/idle-tool/idle-tool';
|
||||
export type {
|
||||
BashToolInput,
|
||||
BashToolOutput,
|
||||
} from './tools/file-tools/bash-tool/bash-tool';
|
||||
export type {
|
||||
GrepToolInput,
|
||||
GrepToolOutput,
|
||||
} from './tools/file-tools/grep-tool/grep-tool';
|
||||
export type {
|
||||
ReadFileToolInput,
|
||||
ReadFileToolOutput,
|
||||
} from './tools/file-tools/read-file-tool/read-file-tool';
|
||||
export type {
|
||||
WriteFileToolInput,
|
||||
WriteFileToolOutput,
|
||||
} from './tools/file-tools/write-file-tool/write-file-tool';
|
||||
export type {
|
||||
EditFileToolInput,
|
||||
EditFileToolOutput,
|
||||
} from './tools/file-tools/edit-file-tool/edit-file-tool';
|
||||
export type {
|
||||
LsToolInput,
|
||||
LsToolOutput,
|
||||
} from './tools/file-tools/ls-tool/ls-tool';
|
||||
|
||||
// Export typed tool events for type-safe tool callbacks
|
||||
export type { ToolEvent, ToolEventCallback } from './agents/analytics-engineer-agent/tool-events';
|
||||
|
|
|
@ -19,6 +19,7 @@ const IdleOutputSchema = z.object({
|
|||
// Optional context for consistency with other tools
|
||||
const IdleContextSchema = z.object({
|
||||
messageId: z.string().optional().describe('The message ID for tracking tool execution.'),
|
||||
onToolEvent: z.any().optional(),
|
||||
});
|
||||
|
||||
export type IdleInput = z.infer<typeof IdleInputSchema>;
|
||||
|
@ -29,18 +30,28 @@ async function processIdle(): Promise<IdleOutput> {
|
|||
return { success: true };
|
||||
}
|
||||
|
||||
function createIdleExecute() {
|
||||
function createIdleExecute<TAgentContext extends IdleContext = IdleContext>(context?: TAgentContext) {
|
||||
return wrapTraced(
|
||||
async (): Promise<IdleOutput> => {
|
||||
return await processIdle();
|
||||
async (args: IdleInput) => {
|
||||
const result: IdleOutput = await processIdle();
|
||||
|
||||
// Emit typed tool event when idle tool completes
|
||||
context?.onToolEvent?.({
|
||||
tool: 'idleTool',
|
||||
event: 'complete',
|
||||
result,
|
||||
args,
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
{ name: 'idle-tool' }
|
||||
);
|
||||
}
|
||||
|
||||
// Factory: simple tool without streaming lifecycle
|
||||
export function createIdleTool() {
|
||||
const execute = createIdleExecute();
|
||||
export function createIdleTool<TAgentContext extends IdleContext = IdleContext>(context?: TAgentContext) {
|
||||
const execute = createIdleExecute(context);
|
||||
|
||||
return tool({
|
||||
description:
|
||||
|
|
|
@ -22,7 +22,8 @@ async function executeCommand(
|
|||
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
||||
|
||||
// Execute command using Bun.spawn
|
||||
const proc = Bun.spawn(['bash', '-c', command], {
|
||||
// Use full path to bash for reliability
|
||||
const proc = Bun.spawn(['/bin/bash', '-c', command], {
|
||||
cwd: projectDirectory,
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
|
@ -113,11 +114,18 @@ async function executeCommand(
|
|||
*/
|
||||
export function createBashToolExecute(context: BashToolContext) {
|
||||
return async function execute(input: BashToolInput): Promise<BashToolOutput> {
|
||||
const { messageId, projectDirectory } = context;
|
||||
const { messageId, projectDirectory, onToolEvent } = context;
|
||||
const { command, timeout } = input;
|
||||
|
||||
console.info(`Executing bash command for message ${messageId}: ${command}`);
|
||||
|
||||
// Emit start event
|
||||
onToolEvent?.({
|
||||
tool: 'bashTool',
|
||||
event: 'start',
|
||||
args: input,
|
||||
});
|
||||
|
||||
const commandTimeout = Math.min(timeout ?? DEFAULT_TIMEOUT, MAX_TIMEOUT);
|
||||
const result = await executeCommand(command, commandTimeout, projectDirectory);
|
||||
|
||||
|
@ -128,6 +136,14 @@ export function createBashToolExecute(context: BashToolContext) {
|
|||
console.error(`Command failed: ${command}`, result.error);
|
||||
}
|
||||
|
||||
// Emit complete event
|
||||
onToolEvent?.({
|
||||
tool: 'bashTool',
|
||||
event: 'complete',
|
||||
result,
|
||||
args: input,
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export const BashToolOutputSchema = z.object({
|
|||
const BashToolContextSchema = z.object({
|
||||
messageId: z.string().describe('The message ID for database updates'),
|
||||
projectDirectory: z.string().describe('The root directory of the project'),
|
||||
onToolEvent: z.any().optional(),
|
||||
});
|
||||
|
||||
export type BashToolInput = z.infer<typeof BashToolInputSchema>;
|
||||
|
|
|
@ -91,7 +91,7 @@ async function executeRipgrep(
|
|||
*/
|
||||
export function createGrepSearchToolExecute(context: GrepToolContext) {
|
||||
return async function execute(input: GrepToolInput): Promise<GrepToolOutput> {
|
||||
const { messageId, projectDirectory } = context;
|
||||
const { messageId, projectDirectory, onToolEvent } = context;
|
||||
const { pattern, path, glob } = input;
|
||||
|
||||
if (!pattern) {
|
||||
|
@ -102,6 +102,13 @@ export function createGrepSearchToolExecute(context: GrepToolContext) {
|
|||
|
||||
console.info(`Searching for pattern "${pattern}" in ${searchPath} for message ${messageId}`);
|
||||
|
||||
// Emit start event
|
||||
onToolEvent?.({
|
||||
tool: 'grepTool',
|
||||
event: 'start',
|
||||
args: input,
|
||||
});
|
||||
|
||||
try {
|
||||
// Execute ripgrep
|
||||
const matches = await executeRipgrep(pattern, searchPath, glob);
|
||||
|
@ -117,23 +124,43 @@ export function createGrepSearchToolExecute(context: GrepToolContext) {
|
|||
`Search complete: ${finalMatches.length} matches found${truncated ? ' (truncated)' : ''}`
|
||||
);
|
||||
|
||||
return {
|
||||
const result = {
|
||||
pattern,
|
||||
matches: finalMatches,
|
||||
totalMatches: finalMatches.length,
|
||||
truncated,
|
||||
};
|
||||
|
||||
// Emit complete event
|
||||
onToolEvent?.({
|
||||
tool: 'grepTool',
|
||||
event: 'complete',
|
||||
result,
|
||||
args: input,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`Grep search failed:`, errorMessage);
|
||||
|
||||
// Return empty results on error
|
||||
return {
|
||||
const result = {
|
||||
pattern,
|
||||
matches: [],
|
||||
totalMatches: 0,
|
||||
truncated: false,
|
||||
};
|
||||
|
||||
// Emit complete event even on error
|
||||
onToolEvent?.({
|
||||
tool: 'grepTool',
|
||||
event: 'complete',
|
||||
result,
|
||||
args: input,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ const GrepToolOutputSchema = z.object({
|
|||
const GrepSearchContextSchema = z.object({
|
||||
messageId: z.string().describe('The message ID for database updates'),
|
||||
projectDirectory: z.string().describe('The root directory of the project'),
|
||||
onToolEvent: z.any().optional(),
|
||||
});
|
||||
|
||||
export type GrepToolInput = z.infer<typeof GrepToolInputSchema>;
|
||||
|
|
|
@ -159,7 +159,7 @@ function renderDir(
|
|||
*/
|
||||
export function createLsToolExecute(context: LsToolContext) {
|
||||
return async function execute(input: LsToolInput): Promise<LsToolOutput> {
|
||||
const { messageId, projectDirectory } = context;
|
||||
const { messageId, projectDirectory, onToolEvent } = context;
|
||||
const searchPath = path.resolve(
|
||||
projectDirectory,
|
||||
input.path || projectDirectory
|
||||
|
@ -167,11 +167,18 @@ export function createLsToolExecute(context: LsToolContext) {
|
|||
|
||||
console.info(`Listing directory ${searchPath} for message ${messageId}`);
|
||||
|
||||
// Emit start event
|
||||
onToolEvent?.({
|
||||
tool: 'lsTool',
|
||||
event: 'start',
|
||||
args: input,
|
||||
});
|
||||
|
||||
try {
|
||||
// Validate the path exists and is a directory
|
||||
const stats = await stat(searchPath);
|
||||
if (!stats.isDirectory()) {
|
||||
return {
|
||||
const result = {
|
||||
success: false,
|
||||
path: searchPath,
|
||||
output: '',
|
||||
|
@ -179,6 +186,16 @@ export function createLsToolExecute(context: LsToolContext) {
|
|||
truncated: false,
|
||||
errorMessage: `Path is not a directory: ${searchPath}`,
|
||||
};
|
||||
|
||||
// Emit complete event
|
||||
onToolEvent?.({
|
||||
tool: 'lsTool',
|
||||
event: 'complete',
|
||||
result,
|
||||
args: input,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build ignore patterns
|
||||
|
@ -225,19 +242,29 @@ export function createLsToolExecute(context: LsToolContext) {
|
|||
`Listed ${files.length} file(s) in ${searchPath}${files.length >= LIMIT ? ' (truncated)' : ''}`
|
||||
);
|
||||
|
||||
return {
|
||||
const result = {
|
||||
success: true,
|
||||
path: searchPath,
|
||||
output,
|
||||
count: files.length,
|
||||
truncated: files.length >= LIMIT,
|
||||
};
|
||||
|
||||
// Emit complete event
|
||||
onToolEvent?.({
|
||||
tool: 'lsTool',
|
||||
event: 'complete',
|
||||
result,
|
||||
args: input,
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error';
|
||||
console.error(`Error listing directory ${searchPath}:`, errorMessage);
|
||||
|
||||
return {
|
||||
const result = {
|
||||
success: false,
|
||||
path: searchPath,
|
||||
output: '',
|
||||
|
@ -245,6 +272,16 @@ export function createLsToolExecute(context: LsToolContext) {
|
|||
truncated: false,
|
||||
errorMessage,
|
||||
};
|
||||
|
||||
// Emit complete event even on error
|
||||
onToolEvent?.({
|
||||
tool: 'lsTool',
|
||||
event: 'complete',
|
||||
result,
|
||||
args: input,
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -28,6 +28,7 @@ export const LsToolOutputSchema = z.object({
|
|||
export const LsToolContextSchema = z.object({
|
||||
messageId: z.string().describe('The message ID for database updates'),
|
||||
projectDirectory: z.string().describe('The root directory of the project'),
|
||||
onToolEvent: z.any().optional(),
|
||||
});
|
||||
|
||||
export type LsToolInput = z.infer<typeof LsToolInputSchema>;
|
||||
|
|
Loading…
Reference in New Issue