import { AgentToolContextFactory } from '../agent-tools/agent-tool.types'; import type { AiChatServiceContext, AiSseEmitter, ModelToolCall } from './ai-chat.types'; import type { AiToolRun } from './entities'; import { executePreflightImport, executeRenderChart, executeRenderForm, executeStartImportWizard, } from './ai-chat.tool-actions'; export type AgentToolContext = ReturnType; export async function startToolRun( context: AiChatServiceContext, messageId: number, call: ModelToolCall, emit: AiSseEmitter, options: { toolName: string; skillKey: string | null; argumentsData?: Record | null; parsedArgs?: unknown; }, ): Promise<{ run: AiToolRun; parsedArgs: unknown; startedAt: number }> { const startedAt = Date.now(); const parsedArgs = options.parsedArgs ?? context.parseToolArguments(call.arguments); const run = await context.toolRuns.save( context.toolRuns.create({ messageId, toolCallId: call.id.slice(0, 100), toolName: options.toolName, skillKey: options.skillKey, argumentsSummary: context.summarize(parsedArgs), resultSummary: null, argumentsData: options.argumentsData ?? (context.safeStructured(parsedArgs) as Record | null), resultData: null, status: 'running', durationMs: null, }), ); emit('tool.started', { messageId, toolCallId: call.id, toolName: run.toolName, skillKey: run.skillKey, status: 'running', summary: run.argumentsSummary, }); return { run, parsedArgs, startedAt }; } export async function finishToolRun( context: AiChatServiceContext, run: AiToolRun, call: ModelToolCall, startedAt: number, outcome: { status: 'success' | 'failed'; summary: string | null; error?: string }, emit: AiSseEmitter, ): Promise { run.status = outcome.status; run.resultSummary = outcome.summary; run.durationMs = Date.now() - startedAt; await context.toolRuns.save(run); emit(outcome.status === 'success' ? 'tool.completed' : 'tool.failed', { messageId: run.messageId, toolCallId: call.id, toolName: run.toolName, skillKey: run.skillKey, status: outcome.status, summary: outcome.summary, ...(outcome.error ? { error: outcome.error } : {}), durationMs: run.durationMs, }); } export async function executeTool( context: AiChatServiceContext, messageId: number, call: ModelToolCall, agentContext: AgentToolContext, allowedSkillKey: string | null, allowWriteTools: boolean, reviewSubmitted: boolean, userId: number, emit: AiSseEmitter, ): Promise { if (call.name === 'render_form') { return executeRenderForm(context, messageId, call, userId, emit); } if (call.name === 'preflight_import') { return executePreflightImport(context, messageId, call, agentContext, emit); } if (call.name === 'start_import_wizard') { return executeStartImportWizard(context, messageId, call, agentContext, emit); } if (call.name === 'render_chart') { return executeRenderChart(context, messageId, call, emit); } if ((call.name === 'create_student' || call.name === 'update_students') && !allowWriteTools) { return denyWriteTool(context, messageId, call, emit); } const toolSkillKey = context.toolExecutor.listAvailable(agentContext).find((tool) => tool.name === call.name) ?.skillKey ?? allowedSkillKey; const { run, parsedArgs, startedAt } = await startToolRun(context, messageId, call, emit, { toolName: context.safeToolName(call.name), skillKey: toolSkillKey, }); const result = await context.toolExecutor.execute(call.name, parsedArgs, agentContext, allowedSkillKey); run.status = result.status; run.skillKey = result.skillKey ?? run.skillKey; run.resultSummary = context.summarize(result.result ?? result.error ?? null); run.resultData = context.safeStructured(result.result) as | Record | unknown[] | null; run.durationMs = Date.now() - startedAt; await context.toolRuns.save(run); emit(result.status === 'success' ? 'tool.completed' : 'tool.failed', { messageId, toolCallId: call.id, toolName: run.toolName, skillKey: run.skillKey, status: result.status, summary: run.resultSummary, ...(result.error ? { error: result.error } : {}), durationMs: run.durationMs, }); const modelPayload = JSON.stringify( result.status === 'success' ? { status: result.status, data: result.result } : { status: result.status, error: result.error }, ); if (modelPayload.length <= 32 * 1024) return modelPayload; return JSON.stringify({ status: result.status, truncated: true, summary: context.summarize(result.result ?? result.error ?? null), }); } export async function denyWriteTool( context: AiChatServiceContext, messageId: number, call: ModelToolCall, emit: AiSseEmitter, ): Promise { const toolName = typeof call.name === 'string' && call.name.trim() ? call.name : 'create_student'; return denyTool(context, messageId, call, toolName, '该操作需要表单确认', '该操作需要表单确认', emit); } export async function denyTool( context: AiChatServiceContext, messageId: number, call: ModelToolCall, toolName: string, summary: string, error: string, emit: AiSseEmitter, ): Promise { await context.toolRuns.save( context.toolRuns.create({ messageId, toolCallId: call.id.slice(0, 100), toolName: context.safeToolName(toolName), skillKey: null, argumentsSummary: context.summarize(context.parseToolArguments(call.arguments)), resultSummary: summary, argumentsData: null, resultData: null, status: 'failed', durationMs: 0, }), ); emit('tool.failed', { messageId, toolCallId: call.id, toolName: context.safeToolName(toolName), status: 'failed', summary, error, durationMs: 0, }); return JSON.stringify({ status: 'failed', error }); }