Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
224 lines
7.9 KiB
JavaScript
224 lines
7.9 KiB
JavaScript
/**
|
|
* Miscellaneous Tool Handlers
|
|
*
|
|
* Handles: view_conversation_steps, view_step_detail, run_code, validate_code,
|
|
* run_code_loop, get_api_documentation, manage_plan, save_conversation_summary
|
|
*/
|
|
|
|
import { dbGet, dbRun, dbAll, isPostgres, safeJsonParse } from '../../database/connection.js';
|
|
import { aiLogger } from '../../utils/logger.js';
|
|
import { executeCodeTool } from '../CodeToolsService.js';
|
|
import { handleManagePlan } from '../chat/agent-execution-shared.js';
|
|
|
|
/**
|
|
* Miscellaneous tool handlers
|
|
*/
|
|
export const miscToolHandlers = {
|
|
// === CONVERSATION INTROSPECTION ===
|
|
async view_conversation_steps({ conversation_id }) {
|
|
const steps = await dbAll(
|
|
`SELECT id, content_type, role, content, tool_results, agent_id, created_at
|
|
FROM messages
|
|
WHERE conversation_id = ? AND content_type IN ('thinking', 'tool_call', 'tool_result')
|
|
ORDER BY created_at ASC`,
|
|
[conversation_id]
|
|
);
|
|
|
|
return {
|
|
success: true,
|
|
conversation_id,
|
|
total_steps: steps.length,
|
|
steps: steps.map((s, i) => {
|
|
const toolData = s.tool_results ? safeJsonParse(s.tool_results) : null;
|
|
const toolName = toolData?.tool || (s.content_type === 'tool_call' ? s.content : null);
|
|
return {
|
|
index: i + 1,
|
|
message_id: s.id,
|
|
type: s.content_type,
|
|
tool: toolName || null,
|
|
preview: s.content?.substring(0, 150) || '',
|
|
timestamp: s.created_at
|
|
};
|
|
})
|
|
};
|
|
},
|
|
|
|
async view_step_detail({ message_id }) {
|
|
const msg = await dbGet(
|
|
`SELECT id, conversation_id, content_type, role, content, tool_results,
|
|
agent_id, model_used, tokens_in, tokens_out, created_at
|
|
FROM messages WHERE id = ?`,
|
|
[message_id]
|
|
);
|
|
|
|
if (!msg) return { error: `Message ${message_id} not found` };
|
|
|
|
const toolData = msg.tool_results ? safeJsonParse(msg.tool_results) : null;
|
|
|
|
return {
|
|
success: true,
|
|
message_id: msg.id,
|
|
conversation_id: msg.conversation_id,
|
|
type: msg.content_type,
|
|
role: msg.role,
|
|
tool: toolData?.tool || null,
|
|
args: toolData?.args || null,
|
|
content: msg.content,
|
|
result: toolData?.result || null,
|
|
model: msg.model_used,
|
|
tokens: msg.tokens_in || msg.tokens_out ? { in: msg.tokens_in, out: msg.tokens_out } : null,
|
|
timestamp: msg.created_at
|
|
};
|
|
},
|
|
|
|
// === CODE EXECUTION (ADR-032) ===
|
|
async run_code(args, context) {
|
|
return executeCodeTool('run_code', args, context);
|
|
},
|
|
|
|
async validate_code(args, context) {
|
|
return executeCodeTool('validate_code', args, context);
|
|
},
|
|
|
|
async run_code_loop(args, context) {
|
|
return executeCodeTool('run_code_loop', args, context);
|
|
},
|
|
|
|
// === API DOCUMENTATION (ADR-036) ===
|
|
async get_api_documentation({ include_tools = true, include_endpoints = true, filter_tag } = {}) {
|
|
try {
|
|
// Lazy import to avoid circular dependency
|
|
const { swaggerOptions } = await import('../../swagger.config.js');
|
|
const swaggerJsdoc = (await import('swagger-jsdoc')).default;
|
|
const swaggerSpec = swaggerJsdoc(swaggerOptions);
|
|
|
|
const result = {
|
|
api_version: swaggerSpec.info?.version || '0.003.001',
|
|
base_url: '/api/v3',
|
|
auth_type: 'Bearer JWT in Authorization header'
|
|
};
|
|
|
|
// Include endpoints
|
|
if (include_endpoints) {
|
|
let paths = swaggerSpec.paths || {};
|
|
|
|
// Filter by tag if specified
|
|
if (filter_tag) {
|
|
const filteredPaths = {};
|
|
for (const [path, methods] of Object.entries(paths)) {
|
|
const filteredMethods = {};
|
|
for (const [method, def] of Object.entries(methods)) {
|
|
if (def.tags && def.tags.includes(filter_tag)) {
|
|
filteredMethods[method] = def;
|
|
}
|
|
}
|
|
if (Object.keys(filteredMethods).length > 0) {
|
|
filteredPaths[path] = filteredMethods;
|
|
}
|
|
}
|
|
paths = filteredPaths;
|
|
}
|
|
|
|
result.endpoints = paths;
|
|
result.endpoint_count = Object.keys(paths).length;
|
|
result.available_tags = (swaggerSpec.tags || []).map(t => ({ name: t.name, description: t.description }));
|
|
}
|
|
|
|
// Include agent tools — need lazy reference to AGENT_TOOLS
|
|
if (include_tools) {
|
|
const { AGENT_TOOLS } = await import('./tool-definitions.js');
|
|
result.agent_tools = AGENT_TOOLS.filter(t => t.function?.name !== 'get_api_documentation').map(tool => ({
|
|
name: tool.function?.name,
|
|
description: tool.function?.description,
|
|
parameters: tool.function?.parameters
|
|
}));
|
|
result.tool_count = result.agent_tools.length;
|
|
}
|
|
|
|
// Usage instructions
|
|
result.usage = {
|
|
response_format: '{ success: boolean, data: any, timestamp: string }',
|
|
common_operations: {
|
|
'List resources': 'GET /resource',
|
|
'Get single': 'GET /resource/:id',
|
|
'Create': 'POST /resource',
|
|
'Update': 'PUT /resource/:id',
|
|
'Delete': 'DELETE /resource/:id'
|
|
}
|
|
};
|
|
|
|
return result;
|
|
} catch (error) {
|
|
aiLogger.error({ err: error }, 'Error getting API documentation');
|
|
return { error: 'Failed to load API documentation: ' + error.message };
|
|
}
|
|
},
|
|
|
|
// === PLANNING TOOL (ADR-113) ===
|
|
// manage_plan is handled via the shared handleManagePlan() from
|
|
// agent-execution-shared.js. This toolHandlers entry delegates to it
|
|
// in validation-only mode (no conversationId).
|
|
async manage_plan(args) {
|
|
return handleManagePlan(args, null, 'unknown');
|
|
},
|
|
|
|
// === SUMMARY TOOL ===
|
|
async save_conversation_summary({ conversation_id, summary_text }, userId) {
|
|
if (!conversation_id || !summary_text) {
|
|
return { error: 'conversation_id and summary_text are required' };
|
|
}
|
|
|
|
try {
|
|
const pg = isPostgres();
|
|
|
|
// 1. Verify conversation exists
|
|
const conv = await dbGet(
|
|
pg ? 'SELECT id, agent_id FROM conversations WHERE id = $1'
|
|
: 'SELECT id, agent_id FROM conversations WHERE id = ?',
|
|
[conversation_id]
|
|
);
|
|
if (!conv) {
|
|
return { error: `Conversation ${conversation_id} not found` };
|
|
}
|
|
|
|
// 2. Save summary as a message from the Summary Agent (SUMMARY_AGENT_ID = 85572)
|
|
const SUMMARY_AGENT_ID = 85572;
|
|
const metadata = JSON.stringify({
|
|
is_summary: true,
|
|
agent_row_id: SUMMARY_AGENT_ID,
|
|
generated_at: new Date().toISOString(),
|
|
});
|
|
|
|
const messageResult = await dbRun(
|
|
pg ? `INSERT INTO messages (conversation_id, role, content, content_type, sender_type, agent_id, metadata, created_at)
|
|
VALUES ($1, 'assistant', $2, 'text', 'agent', $3, $4, NOW())
|
|
RETURNING id`
|
|
: `INSERT INTO messages (conversation_id, role, content, content_type, sender_type, agent_id, metadata, created_at)
|
|
VALUES (?, 'assistant', ?, 'text', 'agent', ?, ?, datetime('now'))`,
|
|
[conversation_id, summary_text, SUMMARY_AGENT_ID, metadata]
|
|
);
|
|
|
|
const messageId = pg ? messageResult?.rows?.[0]?.id : messageResult?.lastID;
|
|
|
|
// 3. Update conversations table — overwrite summary, save message link
|
|
await dbRun(
|
|
pg ? `UPDATE conversations SET summary = $1, summary_message_id = $2, updated_at = NOW() WHERE id = $3`
|
|
: `UPDATE conversations SET summary = ?, summary_message_id = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
[summary_text, messageId, conversation_id]
|
|
);
|
|
|
|
aiLogger.info({ conversation_id, messageId }, 'Conversation summary saved');
|
|
|
|
return {
|
|
success: true,
|
|
message_id: messageId,
|
|
conversation_id,
|
|
summary_length: summary_text.length,
|
|
note: 'Summary saved to conversation. Previous summary overwritten. Old summary accessible via message history.'
|
|
};
|
|
} catch (err) {
|
|
aiLogger.error({ err, conversation_id }, 'Failed to save conversation summary');
|
|
return { error: `Failed to save summary: ${err.message}` };
|
|
}
|
|
}
|
|
};
|