godcrm/backend/services/AgentActivityLogger.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

227 lines
7.6 KiB
JavaScript

/**
* AgentActivityLogger - Logs agent actions to the Agent Activity table (1701)
* Ticket #40813: Agent Activity logging for audit trail and monitoring.
*
* All logging is fire-and-forget (non-blocking): callers should NOT await
* the returned promise unless they need the row ID for chaining.
*
* Table 1701 columns (column names from API):
* agent_id (select) - Agent identifier (e.g. "developer-ralph")
* task_id (number) - Related task/ticket ID
* action (select) - Action type (task_started, message_sent, etc.)
* timestamp (datetime) - ISO 8601 timestamp
* details (longtext) - Free-text details / context
* success (checkbox) - Whether the action succeeded
* duration_ms (number) - Duration in milliseconds
* tokens_used (number) - Token consumption
* cost_usd (number) - Estimated cost in USD
* error_message (text) - Error message if action failed
*/
import { dbRun, sqlNow } from '../database/connection.js';
import { generateBaseId } from '../utils/baseId.js';
import { aiLogger } from '../utils/logger.js';
/** The universal_tables ID for Agent Activity */
const ACTIVITY_TABLE_ID = 1701;
/**
* Valid action types matching the select column options in table 1701.
*/
export const ACTION_TYPES = [
'task_created',
'task_assigned',
'task_started',
'task_completed',
'task_failed',
'code_change',
'test_run',
'quality_check_passed',
'quality_check_failed',
'message_sent',
'agent_mentioned',
'error_occurred',
'retry_attempted',
];
/**
* Log an agent activity row into table 1701.
*
* This function is designed to be fire-and-forget: it catches all errors
* internally so it never throws and never blocks the caller.
*
* @param {Object} entry - Activity entry
* @param {string} entry.agent_id - Agent identifier (required)
* @param {string} entry.action - Action type from ACTION_TYPES (required)
* @param {string} [entry.details] - Free-text details
* @param {boolean} [entry.success] - Whether the action succeeded (default true)
* @param {number} [entry.task_id] - Related task/ticket ID
* @param {number} [entry.duration_ms] - Duration in milliseconds
* @param {number} [entry.tokens_used] - Token count
* @param {number} [entry.cost_usd] - Cost in USD
* @param {string} [entry.error_message]- Error message (if failed)
* @param {string} [entry.conversation_id] - Conversation ID (stored in details)
* @returns {Promise<number|null>} Inserted row ID or null on failure
*/
export async function logAgentActivity(entry) {
try {
if (!entry || !entry.agent_id || !entry.action) {
aiLogger.warn({ entry }, 'AgentActivityLogger: missing required fields (agent_id, action)');
return null;
}
// Build the data JSON matching table 1701 column names
const data = {
agent_id: String(entry.agent_id),
action: String(entry.action),
timestamp: new Date().toISOString(),
success: entry.success !== undefined ? entry.success : true,
};
if (entry.details !== undefined) {
// If conversation_id is passed separately, prepend it to details
let detailsText = String(entry.details);
if (entry.conversation_id) {
detailsText = `[conv:${entry.conversation_id}] ${detailsText}`;
}
data.details = detailsText;
} else if (entry.conversation_id) {
data.details = `[conv:${entry.conversation_id}]`;
}
if (entry.task_id !== undefined) data.task_id = Number(entry.task_id);
if (entry.duration_ms !== undefined) data.duration_ms = Number(entry.duration_ms);
if (entry.tokens_used !== undefined) data.tokens_used = Number(entry.tokens_used);
if (entry.cost_usd !== undefined) data.cost_usd = Number(entry.cost_usd);
if (entry.error_message !== undefined) data.error_message = String(entry.error_message);
const baseId = generateBaseId();
const result = await dbRun(
`INSERT INTO table_rows (table_id, base_id, data, created_at, updated_at)
VALUES (?, ?, ?, ${sqlNow()}, ${sqlNow()})`,
[ACTIVITY_TABLE_ID, baseId, JSON.stringify(data)]
);
const rowId = result?.lastID || result?.lastInsertRowid || null;
aiLogger.debug(
{ rowId, agent_id: data.agent_id, action: data.action },
'AgentActivityLogger: row created'
);
return rowId;
} catch (err) {
// Fire-and-forget: never throw, just log the failure
aiLogger.error(
{ err, entry },
'AgentActivityLogger: failed to log activity (non-blocking)'
);
return null;
}
}
/**
* Convenience: log that an agent started processing a message.
*
* @param {string} agentName - Agent name / ID
* @param {number} conversationId - Conversation ID
* @param {string} [details] - Additional context
* @returns {Promise<number|null>}
*/
export function logMessageSent(agentName, conversationId, details) {
return logAgentActivity({
agent_id: agentName,
action: 'message_sent',
conversation_id: conversationId,
details: details || 'Agent responded to message',
success: true,
});
}
/**
* Convenience: log that an agent was mentioned in a conversation.
*
* @param {string} agentName - Agent name / ID
* @param {number} conversationId - Conversation ID
* @param {string} [triggeredBy] - Who mentioned the agent
* @returns {Promise<number|null>}
*/
export function logAgentMentioned(agentName, conversationId, triggeredBy) {
return logAgentActivity({
agent_id: agentName,
action: 'agent_mentioned',
conversation_id: conversationId,
details: triggeredBy ? `Mentioned by user ${triggeredBy}` : 'Agent mentioned',
success: true,
});
}
/**
* Convenience: log that a tool was used by the agent.
*
* @param {string} agentName - Agent name / ID
* @param {string} toolName - Tool that was called
* @param {number} conversationId - Conversation ID
* @param {Object} [extra] - { duration_ms, tokens_used, success }
* @returns {Promise<number|null>}
*/
export function logToolUsed(agentName, toolName, conversationId, extra = {}) {
return logAgentActivity({
agent_id: agentName,
action: 'task_started',
conversation_id: conversationId,
details: `Tool used: ${toolName}`,
success: extra.success !== undefined ? extra.success : true,
duration_ms: extra.duration_ms,
tokens_used: extra.tokens_used,
});
}
/**
* Convenience: log an error that occurred during agent execution.
*
* @param {string} agentName - Agent name / ID
* @param {number} conversationId - Conversation ID
* @param {string|Error} err - Error object or message
* @returns {Promise<number|null>}
*/
export function logAgentError(agentName, conversationId, err) {
const errorMessage = err instanceof Error ? err.message : String(err);
return logAgentActivity({
agent_id: agentName,
action: 'error_occurred',
conversation_id: conversationId,
details: `Error during agent execution`,
success: false,
error_message: errorMessage,
});
}
/**
* Convenience: log task completion by an agent.
*
* @param {string} agentName - Agent name / ID
* @param {number} conversationId - Conversation ID
* @param {Object} [extra] - { duration_ms, tokens_used, cost_usd }
* @returns {Promise<number|null>}
*/
export function logTaskCompleted(agentName, conversationId, extra = {}) {
return logAgentActivity({
agent_id: agentName,
action: 'task_completed',
conversation_id: conversationId,
details: 'Agent completed task',
success: true,
duration_ms: extra.duration_ms,
tokens_used: extra.tokens_used,
cost_usd: extra.cost_usd,
});
}
export default {
logAgentActivity,
logMessageSent,
logAgentMentioned,
logToolUsed,
logAgentError,
logTaskCompleted,
ACTION_TYPES,
};