From 57985ce87b96b8fa4a6852974b857bacec7b6c29 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 18 May 2026 19:31:48 +0300 Subject: [PATCH] Persist tool loop summary artifact --- PIPELINE_TODO.md | 2 +- src/ai/tool-loop-artifact-store.ts | 39 ++++++++++++++++++++++++++ src/ai/unified-ai-response-pipeline.ts | 19 ++++++++++++- 3 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/ai/tool-loop-artifact-store.ts diff --git a/PIPELINE_TODO.md b/PIPELINE_TODO.md index 9a98198..2e43712 100644 --- a/PIPELINE_TODO.md +++ b/PIPELINE_TODO.md @@ -85,7 +85,7 @@ - [x] Stage `tool_loop` должен выполнять tools через общий `executeToolBatch`. - [x] Stage `tool_loop` должен добавлять tool results в provider adapter. - [x] Stage `tool_loop` должен управлять max rounds. -- [ ] Stage `tool_loop` должен сохранять tool result artifacts. +- [x] Stage `tool_loop` должен сохранять tool result artifacts. - [x] Stage `tool_loop` должен уметь завершаться без tools как `skipped`. - [ ] Убрать tool loop из `runOpenAi`. - [ ] Убрать tool loop из `runMistral`. diff --git a/src/ai/tool-loop-artifact-store.ts b/src/ai/tool-loop-artifact-store.ts new file mode 100644 index 0000000..1049765 --- /dev/null +++ b/src/ai/tool-loop-artifact-store.ts @@ -0,0 +1,39 @@ +import type {StoredAttachment} from "../model/stored-attachment"; +import type {TelegramOutputAttachmentRecord, TelegramToolExecutionRecord} from "./telegram-stream-message.js"; +import {persistInternalJsonArtifactAttachment} from "./internal-artifact-store"; + +export async function persistToolLoopSummaryArtifactAttachment(params: { + chatId: number; + messageId: number; + text: string; + executions: readonly TelegramToolExecutionRecord[]; + outputAttachments: readonly TelegramOutputAttachmentRecord[]; +}): Promise { + if (!params.executions.length) return undefined; + + return await persistInternalJsonArtifactAttachment({ + artifactKind: "tool_result", + fileNamePrefix: "tool-loop-summary", + chatId: params.chatId, + messageId: params.messageId, + payload: { + stage: "tool_loop", + text: params.text.trim(), + executions: params.executions.map(execution => ({ + toolName: execution.toolName, + callId: execution.callId, + argumentsText: execution.argumentsText, + resultChars: execution.resultChars, + startedAt: execution.startedAt, + finishedAt: execution.finishedAt, + })), + outputAttachments: params.outputAttachments, + }, + metadata: { + stage: "tool_loop", + toolExecutions: params.executions.length, + outputAttachments: params.outputAttachments.length, + textChars: params.text.trim().length, + }, + }); +} diff --git a/src/ai/unified-ai-response-pipeline.ts b/src/ai/unified-ai-response-pipeline.ts index 4e96e90..8f6d4a4 100644 --- a/src/ai/unified-ai-response-pipeline.ts +++ b/src/ai/unified-ai-response-pipeline.ts @@ -23,6 +23,7 @@ import {runOllama} from "./unified-ai-runner.ollama"; import {runMistral} from "./unified-ai-runner.mistral"; import {summarizeModelOutput} from "./response-model-output"; import {summarizeToolLoop} from "./tool-loop-summary"; +import {persistToolLoopSummaryArtifactAttachment} from "./tool-loop-artifact-store"; import { resolveTextToSpeechProviderForUser, sendSynthesizedSpeech, @@ -270,15 +271,31 @@ export async function runUnifiedAiResponsePipeline(params: { name: "tool_loop", async run() { const executions = streamMessage.getToolExecutions(); + const outputAttachments = streamMessage.getOutputAttachments(); const summary = summarizeToolLoop({ text: streamMessage.getText(), executions, - outputAttachments: streamMessage.getOutputAttachments(), + outputAttachments, }); + const persisted = await persistToolLoopSummaryArtifactAttachment({ + chatId: options.msg.chat.id, + messageId: options.msg.message_id, + text: streamMessage.getText(), + executions, + outputAttachments, + }); + + if (persisted) { + await streamMessage.storeInternalAttachment(persisted); + } return { stage: "tool_loop", ...summary, + details: { + ...summary.details, + persistedSummaryArtifact: !!persisted, + }, }; }, },