@probelabs/probe
Version:
Node.js wrapper for the probe code search tool
454 lines (385 loc) • 11.6 kB
JavaScript
// ACP Server - Main server implementation for Agent Client Protocol
import { randomUUID } from 'crypto';
import { ACPConnection } from './connection.js';
import { ProbeAgent } from '../ProbeAgent.js';
import {
ACP_PROTOCOL_VERSION,
RequestMethod,
NotificationMethod,
ToolCallStatus,
ToolCallKind,
ErrorCode,
SessionMode,
createTextContent,
createToolCallProgress
} from './types.js';
/**
* ACP Session represents a conversation context
*/
class ACPSession {
constructor(id, mode = SessionMode.NORMAL) {
this.id = id;
this.mode = mode;
this.agent = null;
this.history = [];
this.toolCalls = new Map();
this.createdAt = new Date().toISOString();
this.updatedAt = this.createdAt;
}
/**
* Get or create ProbeAgent for this session
*/
async getAgent(config = {}) {
if (!this.agent) {
this.agent = new ProbeAgent({
sessionId: this.id,
...config
});
// Initialize MCP if enabled
await this.agent.initialize();
}
return this.agent;
}
/**
* Update session timestamp
*/
touch() {
this.updatedAt = new Date().toISOString();
}
/**
* Serialize session state
*/
toJSON() {
return {
id: this.id,
mode: this.mode,
historyLength: this.history.length,
toolCallsCount: this.toolCalls.size,
createdAt: this.createdAt,
updatedAt: this.updatedAt
};
}
}
/**
* ACP Server - handles Agent Client Protocol communication
*/
export class ACPServer {
constructor(options = {}) {
this.options = {
debug: process.env.DEBUG === '1',
provider: options.provider || null,
model: options.model || null,
path: options.path || process.cwd(),
allowEdit: options.allowEdit || false,
...options
};
this.connection = null;
this.sessions = new Map();
this.capabilities = this.getCapabilities();
this.initialized = false;
if (this.options.debug) {
console.error('[ACP] Server created with options:', this.options);
}
}
/**
* Get server capabilities
*/
getCapabilities() {
return {
tools: [
{
name: 'search',
description: 'Search for code patterns and content in the repository',
kind: ToolCallKind.search
},
{
name: 'query',
description: 'Perform structural queries using AST patterns',
kind: ToolCallKind.query
},
{
name: 'extract',
description: 'Extract specific code blocks from files',
kind: ToolCallKind.extract
}
],
sessionManagement: true,
streaming: true,
permissions: this.options.allowEdit
};
}
/**
* Start the ACP server
*/
async start() {
this.connection = new ACPConnection(process.stdin, process.stdout);
// Set up message handlers
this.connection.on('request', this.handleRequest.bind(this));
this.connection.on('notification', this.handleNotification.bind(this));
this.connection.on('error', this.handleError.bind(this));
this.connection.on('disconnect', this.handleDisconnect.bind(this));
// Start the connection
this.connection.start();
if (this.options.debug) {
console.error('[ACP] Server started and listening for messages');
}
}
/**
* Handle incoming requests
*/
async handleRequest(message) {
const { method, params, id } = message;
try {
let result;
switch (method) {
case RequestMethod.INITIALIZE:
result = await this.handleInitialize(params);
break;
case RequestMethod.NEW_SESSION:
result = await this.handleNewSession(params);
break;
case RequestMethod.LOAD_SESSION:
result = await this.handleLoadSession(params);
break;
case RequestMethod.SET_SESSION_MODE:
result = await this.handleSetSessionMode(params);
break;
case RequestMethod.PROMPT:
result = await this.handlePrompt(params);
break;
case RequestMethod.CANCEL:
result = await this.handleCancel(params);
break;
default:
throw new Error(`Unknown method: ${method}`);
}
this.connection.sendResponse(id, result);
} catch (error) {
if (this.options.debug) {
console.error(`[ACP] Error handling request ${method}:`, error);
}
let errorCode = ErrorCode.INTERNAL_ERROR;
if (error.message.includes('Unknown method')) {
errorCode = ErrorCode.METHOD_NOT_FOUND;
} else if (error.message.includes('Invalid params')) {
errorCode = ErrorCode.INVALID_PARAMS;
}
this.connection.sendError(id, errorCode, error.message);
}
}
/**
* Handle notifications
*/
async handleNotification(message) {
const { method, params } = message;
if (this.options.debug) {
console.error(`[ACP] Received notification: ${method}`, params);
}
// Handle notifications here if needed
// For now, just log them
}
/**
* Handle initialize request
*/
async handleInitialize(params) {
if (!params || !params.protocolVersion) {
throw new Error('Invalid params: protocolVersion required');
}
if (params.protocolVersion !== ACP_PROTOCOL_VERSION) {
throw new Error(`Unsupported protocol version: ${params.protocolVersion}`);
}
this.initialized = true;
if (this.options.debug) {
console.error('[ACP] Initialized with protocol version:', params.protocolVersion);
}
return {
protocolVersion: ACP_PROTOCOL_VERSION,
serverInfo: {
name: 'probe-agent-acp',
version: '1.0.0',
description: 'Probe AI agent with code search capabilities'
},
capabilities: this.capabilities
};
}
/**
* Handle new session request
*/
async handleNewSession(params) {
const sessionId = params?.sessionId || randomUUID();
const mode = params?.mode || SessionMode.NORMAL;
const session = new ACPSession(sessionId, mode);
this.sessions.set(sessionId, session);
if (this.options.debug) {
console.error(`[ACP] Created new session: ${sessionId} (mode: ${mode})`);
}
return {
sessionId,
mode,
createdAt: session.createdAt
};
}
/**
* Handle load session request
*/
async handleLoadSession(params) {
if (!params || !params.sessionId) {
throw new Error('Invalid params: sessionId required');
}
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
}
if (this.options.debug) {
console.error(`[ACP] Loaded session: ${params.sessionId}`);
}
return session.toJSON();
}
/**
* Handle set session mode request
*/
async handleSetSessionMode(params) {
if (!params || !params.sessionId || !params.mode) {
throw new Error('Invalid params: sessionId and mode required');
}
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
}
session.mode = params.mode;
session.touch();
if (this.options.debug) {
console.error(`[ACP] Set session mode: ${params.sessionId} -> ${params.mode}`);
}
// Notify about session update
if (this.connection) {
this.connection.sendNotification(NotificationMethod.SESSION_UPDATED, {
sessionId: params.sessionId,
mode: params.mode
});
}
return { success: true };
}
/**
* Handle prompt request - main AI interaction
*/
async handlePrompt(params) {
if (!params || !params.sessionId || !params.message) {
throw new Error('Invalid params: sessionId and message required');
}
const session = this.sessions.get(params.sessionId);
if (!session) {
throw new Error(`Session not found: ${params.sessionId}`);
}
session.touch();
// Get or create ProbeAgent for this session
const agent = await session.getAgent({
path: this.options.path,
provider: this.options.provider,
model: this.options.model,
allowEdit: this.options.allowEdit,
enableDelegate: this.options.enableDelegate,
debug: this.options.debug,
enableMcp: this.options.enableMcp,
mcpConfig: this.options.mcpConfig,
mcpConfigPath: this.options.mcpConfigPath
});
if (this.options.debug) {
console.error(`[ACP] Processing prompt for session ${params.sessionId}:`, params.message.substring(0, 100));
}
try {
// Process the message with the ProbeAgent
const response = await agent.answer(params.message);
// Add to session history
session.history.push(
{ role: 'user', content: params.message, timestamp: new Date().toISOString() },
{ role: 'assistant', content: response, timestamp: new Date().toISOString() }
);
// Send the response as content blocks
return {
content: [createTextContent(response)],
sessionId: params.sessionId,
timestamp: new Date().toISOString()
};
} catch (error) {
if (this.options.debug) {
console.error(`[ACP] Error processing prompt:`, error);
}
// Return error as content
return {
content: [createTextContent(`Error: ${error.message}`)],
sessionId: params.sessionId,
timestamp: new Date().toISOString(),
error: true
};
}
}
/**
* Handle cancel request
*/
async handleCancel(params) {
if (!params || !params.sessionId) {
throw new Error('Invalid params: sessionId required');
}
const session = this.sessions.get(params.sessionId);
if (session && session.agent) {
session.agent.cancel();
}
if (this.options.debug) {
console.error(`[ACP] Cancelled operations for session: ${params.sessionId}`);
}
return { success: true };
}
/**
* Handle connection errors
*/
handleError(error) {
if (this.options.debug) {
console.error('[ACP] Connection error:', error);
}
}
/**
* Handle disconnection
*/
handleDisconnect() {
if (this.options.debug) {
console.error('[ACP] Client disconnected');
}
// Clean up sessions and resources
for (const session of this.sessions.values()) {
if (session.agent) {
session.agent.cancel();
}
}
this.sessions.clear();
}
/**
* Send tool call progress notification
*/
sendToolCallProgress(sessionId, toolCallId, status, result = null, error = null) {
const progress = createToolCallProgress(toolCallId, status, result, error);
this.connection.sendNotification(NotificationMethod.TOOL_CALL_PROGRESS, {
sessionId,
...progress
});
}
/**
* Send message chunk for streaming
*/
sendMessageChunk(sessionId, chunk) {
this.connection.sendNotification(NotificationMethod.MESSAGE_CHUNK, {
sessionId,
chunk
});
}
/**
* Get session statistics
*/
getStats() {
return {
sessions: this.sessions.size,
initialized: this.initialized,
capabilities: this.capabilities
};
}
}