mirror of https://github.com/buster-so/buster.git
181 lines
5.6 KiB
TypeScript
181 lines
5.6 KiB
TypeScript
import { randomUUID } from 'node:crypto';
|
|
import { createProxyModel } from '@buster/ai/llm/providers/proxy-model';
|
|
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;
|
|
}
|
|
| {
|
|
kind: 'write';
|
|
event: 'start' | 'complete';
|
|
args: { files: { path: string; content: string }[] };
|
|
result?: { results: Array<{ status: 'success' | 'error'; filePath: string; errorMessage?: string }> };
|
|
}
|
|
| {
|
|
kind: 'edit';
|
|
event: 'start' | 'complete';
|
|
args: { filePath: string; oldString?: string; newString?: string; edits?: Array<{ oldString: string; newString: string }> };
|
|
result?: { success: boolean; filePath: string; diff?: string; finalDiff?: string; message?: string; errorMessage?: string };
|
|
};
|
|
|
|
export interface DocsAgentMessage {
|
|
message: AgentMessage;
|
|
}
|
|
|
|
export interface RunDocsAgentParams {
|
|
userMessage: string;
|
|
onMessage: (message: DocsAgentMessage) => void;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
const { userMessage, onMessage } = params;
|
|
|
|
// Get proxy configuration
|
|
const proxyConfig = await getProxyConfig();
|
|
|
|
// Create proxy model that routes through server
|
|
const proxyModel = createProxyModel({
|
|
baseURL: proxyConfig.baseURL,
|
|
apiKey: proxyConfig.apiKey,
|
|
modelId: 'anthropic/claude-4-sonnet-20250514',
|
|
});
|
|
|
|
// 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: 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
|
|
},
|
|
});
|
|
}
|
|
|
|
// Handle write tool events - only show complete to avoid duplicates
|
|
if (event.tool === 'writeFileTool' && event.event === 'complete') {
|
|
onMessage({
|
|
message: {
|
|
kind: 'write',
|
|
event: 'complete',
|
|
args: event.args,
|
|
result: event.result, // Type-safe: WriteFileToolOutput
|
|
},
|
|
});
|
|
}
|
|
|
|
// Handle edit tool events (both editFileTool and multiEditFileTool) - only show complete to avoid duplicates
|
|
if (event.tool === 'editFileTool' && event.event === 'complete') {
|
|
onMessage({
|
|
message: {
|
|
kind: 'edit',
|
|
event: 'complete',
|
|
args: event.args,
|
|
result: event.result, // Type-safe: EditFileToolOutput or MultiEditFileToolOutput
|
|
},
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
const messages: ModelMessage[] = [
|
|
{
|
|
role: 'user',
|
|
content: userMessage,
|
|
},
|
|
];
|
|
|
|
// Start the stream - this triggers the agent to run
|
|
const stream = await analyticsEngineerAgent.stream({ messages });
|
|
|
|
// 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
|
|
}
|
|
}
|