Summarize tool loop output

This commit is contained in:
2026-05-18 19:05:13 +03:00
parent 9d6cdb008b
commit 9352ade19f
4 changed files with 113 additions and 32 deletions
+2 -2
View File
@@ -81,12 +81,12 @@
- [ ] Stage `model_call` должен делать только один model request. - [ ] Stage `model_call` должен делать только один model request.
- [x] Stage `model_call` должен возвращать normalized model output. - [x] Stage `model_call` должен возвращать normalized model output.
- [ ] Stage `tool_loop` должен решать, есть ли tool calls. - [x] Stage `tool_loop` должен решать, есть ли tool calls.
- [ ] Stage `tool_loop` должен выполнять tools через общий `executeToolBatch`. - [ ] Stage `tool_loop` должен выполнять tools через общий `executeToolBatch`.
- [ ] Stage `tool_loop` должен добавлять tool results в provider adapter. - [ ] Stage `tool_loop` должен добавлять tool results в provider adapter.
- [ ] Stage `tool_loop` должен управлять max rounds. - [ ] Stage `tool_loop` должен управлять max rounds.
- [ ] Stage `tool_loop` должен сохранять tool result artifacts. - [ ] Stage `tool_loop` должен сохранять tool result artifacts.
- [ ] Stage `tool_loop` должен уметь завершаться без tools как `skipped`. - [x] Stage `tool_loop` должен уметь завершаться без tools как `skipped`.
- [ ] Убрать tool loop из `runOpenAi`. - [ ] Убрать tool loop из `runOpenAi`.
- [ ] Убрать tool loop из `runMistral`. - [ ] Убрать tool loop из `runMistral`.
- [ ] Убрать tool loop из `runOllama`. - [ ] Убрать tool loop из `runOllama`.
+56
View File
@@ -0,0 +1,56 @@
import type {PipelineArtifact} from "./user-request-pipeline/types.js";
import type {TelegramOutputAttachmentRecord, TelegramToolExecutionRecord} from "./telegram-stream-message.js";
import {summarizeModelOutput} from "./response-model-output.js";
export type ToolLoopSummary = {
status: "succeeded" | "skipped";
fallbackAction?: "continue_without_stage";
details: {
modelOutput: ReturnType<typeof summarizeModelOutput>;
count: number;
tools: Array<{
toolName: string;
callId: string;
resultChars: number;
}>;
};
artifacts?: PipelineArtifact[];
};
export function summarizeToolLoop(params: {
text: string;
executions: readonly TelegramToolExecutionRecord[];
outputAttachments: readonly TelegramOutputAttachmentRecord[];
}): ToolLoopSummary {
const count = params.executions.length;
const tools = params.executions.map(execution => ({
toolName: execution.toolName,
callId: execution.callId,
resultChars: execution.resultChars,
}));
return {
status: count ? "succeeded" : "skipped",
fallbackAction: count ? undefined : "continue_without_stage",
details: {
modelOutput: summarizeModelOutput({
text: params.text,
toolExecutions: params.executions,
outputAttachments: params.outputAttachments,
}),
count,
tools,
},
artifacts: count ? [{
kind: "tool_result",
stage: "tool_loop",
createdAt: new Date().toISOString(),
toolName: "summary",
callId: "tool_loop_summary",
resultText: JSON.stringify({
count,
tools,
}),
}] : undefined,
};
}
+8 -30
View File
@@ -22,6 +22,7 @@ import {runOpenAi} from "./unified-ai-runner.openai";
import {runOllama} from "./unified-ai-runner.ollama"; import {runOllama} from "./unified-ai-runner.ollama";
import {runMistral} from "./unified-ai-runner.mistral"; import {runMistral} from "./unified-ai-runner.mistral";
import {summarizeModelOutput} from "./response-model-output"; import {summarizeModelOutput} from "./response-model-output";
import {summarizeToolLoop} from "./tool-loop-summary";
import { import {
resolveTextToSpeechProviderForUser, resolveTextToSpeechProviderForUser,
sendSynthesizedSpeech, sendSynthesizedSpeech,
@@ -269,38 +270,15 @@ export async function runUnifiedAiResponsePipeline(params: {
name: "tool_loop", name: "tool_loop",
async run() { async run() {
const executions = streamMessage.getToolExecutions(); const executions = streamMessage.getToolExecutions();
const summary = summarizeToolLoop({
text: streamMessage.getText(),
executions,
outputAttachments: streamMessage.getOutputAttachments(),
});
return { return {
stage: "tool_loop", stage: "tool_loop",
status: executions.length ? "succeeded" : "skipped", ...summary,
fallbackAction: executions.length ? undefined : "continue_without_stage",
details: {
modelOutput: summarizeModelOutput({
text: streamMessage.getText(),
toolExecutions: executions,
outputAttachments: streamMessage.getOutputAttachments(),
}),
count: executions.length,
tools: executions.map(execution => ({
toolName: execution.toolName,
callId: execution.callId,
resultChars: execution.resultChars,
})),
},
artifacts: executions.length ? [{
kind: "tool_result",
stage: "tool_loop",
createdAt: new Date().toISOString(),
toolName: "summary",
callId: "tool_loop_summary",
resultText: JSON.stringify({
count: executions.length,
tools: executions.map(execution => ({
toolName: execution.toolName,
callId: execution.callId,
resultChars: execution.resultChars,
})),
}),
}] : undefined,
}; };
}, },
}, },
+47
View File
@@ -0,0 +1,47 @@
import test from "node:test";
import assert from "node:assert/strict";
const {summarizeToolLoop} = await import("../dist/ai/tool-loop-summary.js");
test("tool loop summary skips empty tool execution batches", () => {
const summary = summarizeToolLoop({
text: "answer",
executions: [],
outputAttachments: [],
});
assert.equal(summary.status, "skipped");
assert.equal(summary.fallbackAction, "continue_without_stage");
assert.equal(summary.details.count, 0);
assert.deepEqual(summary.details.tools, []);
assert.deepEqual(summary.details.modelOutput, {
text: "answer",
toolExecutions: [],
outputAttachments: [],
});
assert.equal(summary.artifacts, undefined);
});
test("tool loop summary reports executions and summary artifact", () => {
const summary = summarizeToolLoop({
text: "answer",
executions: [{
toolName: "read_file",
callId: "call-1",
argumentsText: "{}",
resultChars: 12,
}],
outputAttachments: [],
});
assert.equal(summary.status, "succeeded");
assert.equal(summary.fallbackAction, undefined);
assert.equal(summary.details.count, 1);
assert.deepEqual(summary.details.tools, [{
toolName: "read_file",
callId: "call-1",
resultChars: 12,
}]);
assert.equal(summary.artifacts?.[0]?.kind, "tool_result");
assert.equal(summary.artifacts?.[0]?.stage, "tool_loop");
});