@ykurilov/agent-chat-core 0.58.0 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1853,6 +1853,494 @@ function createBurstWaits() {
1853
1853
  };
1854
1854
  }
1855
1855
 
1856
+ // src/agent-run/forged-blocks.ts
1857
+ import { wrapLanguageModel as wrapLanguageModel2 } from "ai";
1858
+
1859
+ // src/text/markdown-code.ts
1860
+ function codeFenceOf(line) {
1861
+ return /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1] ?? null;
1862
+ }
1863
+ function closesCodeFence(line, fence) {
1864
+ const opener = codeFenceOf(line);
1865
+ return opener !== null && opener[0] === fence[0] && opener.length >= fence.length && line.trim() === opener;
1866
+ }
1867
+ function isIndentedCodeOrQuote(line) {
1868
+ return /^(?: {4}|\t)/.test(line) || /^\s*>/.test(line);
1869
+ }
1870
+
1871
+ // src/agent-run/length-cut.ts
1872
+ var LENGTH_CUT_PART_TYPE = "data-length-cut";
1873
+ var LENGTH_CUT_NOTE = "[\u041E\u0442\u0432\u0435\u0442 \u043E\u0431\u043E\u0440\u0432\u0430\u043D: \u043C\u043E\u0434\u0435\u043B\u044C \u0443\u043F\u0451\u0440\u043B\u0430\u0441\u044C \u0432 \u043F\u0440\u0435\u0434\u0435\u043B \u0434\u043B\u0438\u043D\u044B \u043E\u0442\u0432\u0435\u0442\u0430]";
1874
+ function lengthCutForModel(part) {
1875
+ return part.type === LENGTH_CUT_PART_TYPE ? { type: "text", text: LENGTH_CUT_NOTE } : void 0;
1876
+ }
1877
+
1878
+ // src/agent-run/unanswered-messages.ts
1879
+ var UNANSWERED_NOTE = "[\u041E\u0442\u0432\u0435\u0442\u0430 \u043D\u0430 \u044D\u0442\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043D\u0435\u0442: \u0445\u043E\u0434 \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0441\u044F \u0434\u043E \u043F\u0435\u0440\u0432\u044B\u0445 \u0441\u043B\u043E\u0432]";
1880
+ function withUnansweredMarked(messages) {
1881
+ return messages.flatMap(
1882
+ (message, index) => message.role === "user" && messages[index - 1]?.role === "user" ? [{ role: "assistant", content: [{ type: "text", text: UNANSWERED_NOTE }] }, message] : [message]
1883
+ );
1884
+ }
1885
+
1886
+ // src/agent-run/forged-blocks.ts
1887
+ var CHANNEL_NOTE_LINES = [
1888
+ "\u0420\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0441\u043D\u043E\u0432\u0430 \u0432 \u0432\u0435\u0431-\u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0438: \u043E\u0431\u044B\u0447\u043D\u0430\u044F \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0430 \u0438 \u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B.",
1889
+ "\u0420\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0438\u0434\u0451\u0442 \u0432 Telegram. \u041F\u0438\u0448\u0438 \u043E\u0431\u044B\u0447\u043D\u044B\u043C Markdown: \u0435\u0433\u043E \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0443\u0442 \u0432 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0443 Telegram. \u0422\u0430\u0431\u043B\u0438\u0446 \u0438 \u0441\u0445\u0435\u043C \u0442\u0430\u043C \u043D\u0435\u0442 \u2014 \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0438\u0445 \u0441\u043F\u0438\u0441\u043A\u0438. \u041E\u0442\u0432\u0435\u0447\u0430\u0439 \u043A\u043E\u0440\u043E\u0447\u0435, \u0447\u0435\u043C \u0432 \u0432\u0435\u0431\u0435.",
1890
+ "\u0420\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0438\u0434\u0451\u0442 \u0432\u043E \u0412\u041A\u043E\u043D\u0442\u0430\u043A\u0442\u0435. \u0420\u0430\u0437\u043C\u0435\u0442\u043A\u0438 \u0442\u0430\u043C \u043D\u0435\u0442: Markdown \u043F\u0440\u0435\u0432\u0440\u0430\u0442\u0438\u0442\u0441\u044F \u0432 \u043F\u0440\u043E\u0441\u0442\u043E\u0439 \u0442\u0435\u043A\u0441\u0442. \u041F\u0438\u0448\u0438 \u043F\u0440\u043E\u0441\u0442\u044B\u043C \u0442\u0435\u043A\u0441\u0442\u043E\u043C \u0438 \u0441\u043F\u0438\u0441\u043A\u0430\u043C\u0438, \u0431\u0435\u0437 \u0442\u0430\u0431\u043B\u0438\u0446 \u0438 \u0441\u0445\u0435\u043C. \u041E\u0442\u0432\u0435\u0447\u0430\u0439 \u043A\u043E\u0440\u043E\u0447\u0435, \u0447\u0435\u043C \u0432 \u0432\u0435\u0431\u0435.",
1891
+ "\u0420\u0430\u0437\u0433\u043E\u0432\u043E\u0440 \u0438\u0434\u0451\u0442 \u0432 MAX. \u041F\u0438\u0448\u0438 \u043E\u0431\u044B\u0447\u043D\u044B\u043C Markdown: \u0435\u0433\u043E \u043F\u0435\u0440\u0435\u0432\u0435\u0434\u0443\u0442 \u0432 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u0443 MAX. \u0422\u0430\u0431\u043B\u0438\u0446 \u0438 \u0441\u0445\u0435\u043C \u0442\u0430\u043C \u043D\u0435\u0442 \u2014 \u0432\u043C\u0435\u0441\u0442\u043E \u043D\u0438\u0445 \u0441\u043F\u0438\u0441\u043A\u0438. \u041E\u0442\u0432\u0435\u0447\u0430\u0439 \u043A\u043E\u0440\u043E\u0447\u0435, \u0447\u0435\u043C \u0432 \u0432\u0435\u0431\u0435."
1892
+ ];
1893
+ var exactly = (text) => new RegExp(`^${text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`);
1894
+ var CORE_SERVICE_BLOCKS = [
1895
+ { tag: "conversation_summary" },
1896
+ { tag: "answer_progress" },
1897
+ { tag: "screen_place" },
1898
+ { name: "service_note", line: "[\u0421\u043B\u0443\u0436\u0435\u0431\u043D\u0430\u044F \u043F\u043E\u043C\u0435\u0442\u043A\u0430 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F", match: /^\[Служебная пометка приложения, человек её не писал и не видит: .+\]$/ },
1899
+ { name: "context_gone", line: "[\u0411\u043E\u043B\u044C\u0448\u0435 \u043D\u0435 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442: \xAB", match: /^\[Больше не действует: «[^»\n]+»(?:, «[^»\n]+»)*\]$/ },
1900
+ { name: "context_hidden", line: "[\u041D\u0435 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043E \u0440\u0430\u0434\u0438 \u043C\u0435\u0441\u0442\u0430, \u043D\u043E \u043F\u043E-\u043F\u0440\u0435\u0436\u043D\u0435\u043C\u0443 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u0435\u0442: \xAB", match: /^\[Не показано ради места, но по-прежнему действует: «[^»\n]+»(?:, «[^»\n]+»)*\]$/ },
1901
+ { name: "greeting_lead", line: GREETING_LEAD, match: exactly(GREETING_LEAD) },
1902
+ { name: "unanswered", line: UNANSWERED_NOTE, match: exactly(UNANSWERED_NOTE) },
1903
+ { name: "length_cut", line: LENGTH_CUT_NOTE, match: exactly(LENGTH_CUT_NOTE) },
1904
+ { name: "tool_ended", line: TOOL_ENDED_NOTE, match: exactly(TOOL_ENDED_NOTE) },
1905
+ { name: "switched_off_call", line: "[\u041F\u0440\u043E\u0448\u043B\u044B\u0439 \u0432\u044B\u0437\u043E\u0432 \xAB", match: /^\[Прошлый вызов «[^»\n]*», сейчас выключен\./ },
1906
+ {
1907
+ name: "staff_reply",
1908
+ line: "\u0416\u0438\u0432\u043E\u0439 \u0441\u043E\u0442\u0440\u0443\u0434\u043D\u0438\u043A",
1909
+ match: /^Живой сотрудник(?: «[^»\n]*»)? ответил человеку вместо тебя: «.*»\. Это сказано от вашего имени: продолжай разговор с учётом этого и не противоречь сотруднику\.$/
1910
+ },
1911
+ ...CHANNEL_NOTE_LINES.map((note) => ({ name: "channel_note", line: note, match: exactly(note) })),
1912
+ // Имя автора — коротко и без знаков конца фразы: «Сообщение в группе от участника: Аня».
1913
+ { name: "group_author", line: "\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0432 \u0433\u0440\u0443\u043F\u043F\u0435 \u043E\u0442 \u0443\u0447\u0430\u0441\u0442\u043D\u0438\u043A\u0430: ", match: /^Сообщение в группе от участника: [^\s.!?:;][^.!?:;\n]{0,79}$/ }
1914
+ ];
1915
+ var serviceBlockName = (format) => "tag" in format ? format.tag : format.name;
1916
+ function explainsServiceFormats(question2, formats) {
1917
+ const text = question2.toLowerCase();
1918
+ if (formats.some((format) => "tag" in format && text.includes(format.tag.toLowerCase()))) return true;
1919
+ return /(?:^|[^\p{L}])(?:тег(?:а|е|и|у|ом|ов|ами|ам|ах)?|формат(?:а|е|у|ом|ы|ов|ами|ам|ах)?|разметк[аеиу]|tags?|formats?|markup)(?:[^\p{L}]|$)/u.test(text);
1920
+ }
1921
+ var EXAMPLE_LEAD = /(?:^|[^\p{L}])(?:например|пример\p{L}*|образец|шаблон\p{L}*|выгляд\p{L}*|формат\p{L}*|такой|такая|такое|такие|так|example|e\.g\.|looks like|format)(?:[^\p{L}][^\n]*)?:\s*$/iu;
1922
+ var textOfMessage = (message) => message.parts.map((part) => part.type === "text" ? part.text : "").join("\n");
1923
+ function lastQuestionOf(history) {
1924
+ const question2 = history.findLast((message) => message.role === "user");
1925
+ return question2 ? textOfMessage(question2) : "";
1926
+ }
1927
+ function checkServiceBlockFormats(formats) {
1928
+ for (const format of formats) {
1929
+ const shown = JSON.stringify(format);
1930
+ if (!format || typeof format !== "object") throw new TypeError(`serviceBlocks: \u043D\u0443\u0436\u0435\u043D \u043E\u0431\u044A\u0435\u043A\u0442 \u0444\u043E\u0440\u043C\u0430\u0442\u0430, \u0430 \u043D\u0435 ${shown}`);
1931
+ if ("tag" in format) {
1932
+ if (typeof format.tag !== "string" || !/^[A-Za-z][\w:.-]*$/.test(format.tag)) throw new TypeError(`serviceBlocks: \u0438\u043C\u044F \u0442\u0435\u0433\u0430 \u2014 \u043B\u0430\u0442\u0438\u043D\u0438\u0446\u0430, \u0446\u0438\u0444\u0440\u044B, _ : . -, \u0430 \u043D\u0435 ${shown}`);
1933
+ continue;
1934
+ }
1935
+ if (typeof format.name !== "string" || format.name === "") throw new TypeError(`serviceBlocks: \u0443 \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0438 \u043D\u0443\u0436\u043D\u043E \u0438\u043C\u044F \u0434\u043B\u044F \u0436\u0443\u0440\u043D\u0430\u043B\u0430: ${shown}`);
1936
+ if (typeof format.line !== "string" || format.line.trim() === "") throw new TypeError(`serviceBlocks: \u0443 \u0441\u043B\u0443\u0436\u0435\u0431\u043D\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u0438 \u043D\u0443\u0436\u043D\u043E \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u043D\u0430\u0447\u0430\u043B\u043E line: ${shown}`);
1937
+ if (format.match !== void 0 && !(format.match instanceof RegExp)) throw new TypeError(`serviceBlocks: match \u2014 \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0435 \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u0435: ${format.name}`);
1938
+ if (format.until !== void 0 && format.until !== "paragraph" && !(format.until instanceof RegExp)) {
1939
+ throw new TypeError(`serviceBlocks: until \u2014 "paragraph" \u0438\u043B\u0438 \u0440\u0435\u0433\u0443\u043B\u044F\u0440\u043D\u043E\u0435 \u0432\u044B\u0440\u0430\u0436\u0435\u043D\u0438\u0435: ${format.name}`);
1940
+ }
1941
+ }
1942
+ }
1943
+ var compiledByList = /* @__PURE__ */ new WeakMap();
1944
+ function compile(formats) {
1945
+ const known = compiledByList.get(formats);
1946
+ if (known) return known;
1947
+ const tags = formats.flatMap((format) => "tag" in format ? [format.tag] : []);
1948
+ const lines = formats.filter((format) => !("tag" in format));
1949
+ const escape = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1950
+ const starts = [...tags.map((tag) => `<\\/?${escape(tag)}`), ...lines.map((format) => escape(format.line))];
1951
+ const hint = starts.length > 0 ? new RegExp(`(?:^|\\n) {0,3}(?:${starts.join("|")})`) : /(?!)/;
1952
+ const compiled = { tags, lines, hint };
1953
+ compiledByList.set(formats, compiled);
1954
+ return compiled;
1955
+ }
1956
+ var MAX_ATTRIBUTES_LENGTH = 200;
1957
+ function paragraphEnd(text) {
1958
+ const blank = /\n[ \t]*(?:\n|$)/g;
1959
+ blank.lastIndex = Math.max(0, text.indexOf("\n"));
1960
+ const found = text.indexOf("\n") < 0 ? null : blank.exec(text);
1961
+ return found ? found.index + 1 : -1;
1962
+ }
1963
+ function createForgedBlockScanner(formats, options2 = {}) {
1964
+ const compiled = compile(formats);
1965
+ const lines = compiled.lines;
1966
+ const tags = options2.explaining ? [] : compiled.tags;
1967
+ let fence = null;
1968
+ let previousLine = "";
1969
+ let pending = "";
1970
+ let ordinaryLine = null;
1971
+ let block = null;
1972
+ let skipSpace = false;
1973
+ let out = [];
1974
+ const emitText = (text) => {
1975
+ if (!text) return;
1976
+ const last = out.at(-1);
1977
+ if (last?.kind === "text") last.text += text;
1978
+ else out.push({ kind: "text", text });
1979
+ };
1980
+ const emitGap = (text) => {
1981
+ const last = out.at(-1);
1982
+ if (last?.kind === "gap") last.text += text;
1983
+ else out.push({ kind: "gap", text });
1984
+ };
1985
+ const closeOrdinaryLine = (line) => {
1986
+ if (line.trim() !== "") previousLine = line;
1987
+ if (fence !== null) {
1988
+ if (closesCodeFence(line, fence)) fence = null;
1989
+ } else {
1990
+ const opener = codeFenceOf(line);
1991
+ if (opener) fence = opener;
1992
+ }
1993
+ };
1994
+ function classify(line, complete) {
1995
+ if (fence !== null || isIndentedCodeOrQuote(line) || codeFenceOf(line)) return "text";
1996
+ const indent = /^ {0,3}/.exec(line)[0].length;
1997
+ const body = line.slice(indent);
1998
+ if (body === "") return complete ? "text" : "wait";
1999
+ let waiting = false;
2000
+ if (body[0] === "<") {
2001
+ const closing = body[1] === "/";
2002
+ const nameAt = closing ? 2 : 1;
2003
+ for (const tag of tags) {
2004
+ const head = body.slice(nameAt, nameAt + tag.length);
2005
+ if (!tag.startsWith(head)) continue;
2006
+ if (head.length < tag.length) {
2007
+ waiting ||= body.length === nameAt + head.length;
2008
+ continue;
2009
+ }
2010
+ if (closing) continue;
2011
+ const next = body[nameAt + tag.length];
2012
+ if (next === void 0) {
2013
+ waiting = true;
2014
+ continue;
2015
+ }
2016
+ if (next !== ">" && !/\s/.test(next)) continue;
2017
+ const end = body.indexOf(">", nameAt + tag.length);
2018
+ if (end >= 0 && end - (nameAt + tag.length) <= MAX_ATTRIBUTES_LENGTH && !body.slice(nameAt + tag.length, end).includes("\n")) {
2019
+ if (EXAMPLE_LEAD.test(previousLine.trimEnd())) continue;
2020
+ const close = `</${tag}>`;
2021
+ const rest = body.slice(end + 1);
2022
+ if (rest.includes(close)) return { kind: "tag", name: tag, close, from: indent + end + 1 };
2023
+ if (!complete) {
2024
+ waiting = true;
2025
+ continue;
2026
+ }
2027
+ if (rest.trim() === "" || /^\s*[[{"]/.test(rest)) return { kind: "tag", name: tag, close, from: indent + end + 1 };
2028
+ continue;
2029
+ }
2030
+ if (end < 0 && !complete && body.length - (nameAt + tag.length) <= MAX_ATTRIBUTES_LENGTH) waiting = true;
2031
+ }
2032
+ }
2033
+ for (const format of lines) {
2034
+ if (body.length < format.line.length) {
2035
+ waiting ||= format.line.startsWith(body);
2036
+ continue;
2037
+ }
2038
+ if (!body.startsWith(format.line)) continue;
2039
+ if (!complete) {
2040
+ waiting = true;
2041
+ continue;
2042
+ }
2043
+ if (!format.match || format.match.test(body.trimEnd())) return { kind: "line", format };
2044
+ }
2045
+ return waiting && !complete ? "wait" : "text";
2046
+ }
2047
+ function openBlock(opener, line, newline) {
2048
+ const whole = line + (newline ? "\n" : "");
2049
+ if (opener.kind === "tag") {
2050
+ block = { kind: "tag", name: opener.name, close: opener.close, text: whole, searchFrom: opener.from };
2051
+ return;
2052
+ }
2053
+ const { format } = opener;
2054
+ if (!format.until) {
2055
+ out.push({ kind: "block", name: format.name, text: whole });
2056
+ skipSpace = true;
2057
+ return;
2058
+ }
2059
+ block = format.until === "paragraph" ? { kind: "paragraph", name: format.name, text: whole } : { kind: "until", name: format.name, until: format.until, text: whole };
2060
+ }
2061
+ function feedBlock(current, delta) {
2062
+ current.text += delta;
2063
+ if (current.kind === "tag") {
2064
+ const at = current.text.indexOf(current.close, current.searchFrom);
2065
+ if (at < 0) {
2066
+ current.searchFrom = Math.max(current.searchFrom, current.text.length - current.close.length + 1);
2067
+ return null;
2068
+ }
2069
+ const end = at + current.close.length;
2070
+ return finishBlock(current, end);
2071
+ }
2072
+ let lineStart = current.text.indexOf("\n") + 1;
2073
+ while (lineStart > 0) {
2074
+ const lineEnd = current.text.indexOf("\n", lineStart);
2075
+ if (lineEnd < 0) return null;
2076
+ const line = current.text.slice(lineStart, lineEnd);
2077
+ if (current.kind === "paragraph" && line.trim() === "") return finishBlock(current, lineStart);
2078
+ if (current.kind === "until" && current.until.test(line.trim())) return finishBlock(current, lineEnd + 1);
2079
+ lineStart = lineEnd + 1;
2080
+ }
2081
+ return null;
2082
+ }
2083
+ function finishBlock(current, end) {
2084
+ out.push({ kind: "block", name: current.name, text: current.text.slice(0, end) });
2085
+ block = null;
2086
+ skipSpace = true;
2087
+ previousLine = "";
2088
+ return current.text.slice(end);
2089
+ }
2090
+ function feed(delta) {
2091
+ let rest = delta;
2092
+ while (rest) {
2093
+ if (block) {
2094
+ const left = feedBlock(block, rest);
2095
+ if (left === null) return;
2096
+ rest = left;
2097
+ continue;
2098
+ }
2099
+ if (skipSpace) {
2100
+ const space = /^\s*/.exec(rest)[0];
2101
+ if (space) emitGap(space);
2102
+ rest = rest.slice(space.length);
2103
+ if (!rest) return;
2104
+ skipSpace = false;
2105
+ pending = "";
2106
+ ordinaryLine = null;
2107
+ continue;
2108
+ }
2109
+ if (ordinaryLine !== null) {
2110
+ const newline2 = rest.indexOf("\n");
2111
+ if (newline2 < 0) {
2112
+ ordinaryLine += rest;
2113
+ emitText(rest);
2114
+ return;
2115
+ }
2116
+ closeOrdinaryLine(ordinaryLine + rest.slice(0, newline2));
2117
+ emitText(rest.slice(0, newline2 + 1));
2118
+ ordinaryLine = null;
2119
+ rest = rest.slice(newline2 + 1);
2120
+ continue;
2121
+ }
2122
+ const newline = rest.indexOf("\n");
2123
+ const line = pending + (newline < 0 ? rest : rest.slice(0, newline));
2124
+ const verdict = classify(line, newline >= 0);
2125
+ if (verdict === "wait") {
2126
+ pending = line;
2127
+ return;
2128
+ }
2129
+ pending = "";
2130
+ const after = newline < 0 ? "" : rest.slice(newline + 1);
2131
+ if (verdict === "text") {
2132
+ if (newline < 0) {
2133
+ ordinaryLine = line;
2134
+ emitText(line);
2135
+ return;
2136
+ }
2137
+ closeOrdinaryLine(line);
2138
+ emitText(`${line}
2139
+ `);
2140
+ rest = after;
2141
+ continue;
2142
+ }
2143
+ openBlock(verdict, line, newline >= 0);
2144
+ rest = after;
2145
+ }
2146
+ }
2147
+ const take = () => {
2148
+ const pieces = out;
2149
+ out = [];
2150
+ return pieces;
2151
+ };
2152
+ return {
2153
+ /** Следующий кусок текста → готовые куски. Начало возможного блока придерживается до следующего. */
2154
+ push(delta) {
2155
+ feed(delta);
2156
+ return take();
2157
+ },
2158
+ /**
2159
+ * Текст кончился: придержанное решается на неполной строке. Незакрытый тег — блок до первой пустой строки, остальное
2160
+ * разбирается дальше; строчный блок — до конца текста.
2161
+ */
2162
+ end() {
2163
+ for (; ; ) {
2164
+ const current = block;
2165
+ if (current) {
2166
+ block = null;
2167
+ const blank = current.kind === "tag" ? paragraphEnd(current.text) : -1;
2168
+ if (blank < 0) {
2169
+ out.push({ kind: "block", name: current.name, text: current.text });
2170
+ continue;
2171
+ }
2172
+ finishBlock(current, blank);
2173
+ feed(current.text.slice(blank));
2174
+ continue;
2175
+ }
2176
+ if (!pending) break;
2177
+ const line = pending;
2178
+ pending = "";
2179
+ const verdict = classify(line, true);
2180
+ if (verdict === "text" || verdict === "wait") emitText(line);
2181
+ else openBlock(verdict, line, false);
2182
+ }
2183
+ return take();
2184
+ }
2185
+ };
2186
+ }
2187
+ function cutForgedBlocks(text, formats, options2 = {}) {
2188
+ if (!compile(formats).hint.test(text)) return { text, blocks: [] };
2189
+ const scanner = createForgedBlockScanner(formats, options2);
2190
+ const pieces = [...scanner.push(text), ...scanner.end()];
2191
+ return {
2192
+ text: pieces.map((piece) => piece.kind === "text" ? piece.text : "").join(""),
2193
+ blocks: pieces.flatMap((piece) => piece.kind === "block" ? [piece.name] : [])
2194
+ };
2195
+ }
2196
+ var hasWords = (text) => /[\p{L}\p{N}]/u.test(text);
2197
+ function cutForgedBlocksInParts(parts, formats, options2 = {}) {
2198
+ const blocks = [];
2199
+ const cleaned = parts.flatMap((part) => {
2200
+ if (part.type !== "text") return [part];
2201
+ const cut = cutForgedBlocks(part.text, formats, options2);
2202
+ if (cut.blocks.length === 0) return [part];
2203
+ blocks.push(...cut.blocks);
2204
+ return cut.text.trim() === "" ? [] : [{ ...part, text: cut.text }];
2205
+ });
2206
+ if (blocks.length === 0) return null;
2207
+ if (!cleaned.some((part) => part.type === "text" && hasWords(part.text))) return null;
2208
+ return { parts: cleaned, blocks };
2209
+ }
2210
+ function withoutForgedBlocks(history, formats) {
2211
+ let changed = false;
2212
+ let question2 = "";
2213
+ const cleaned = history.map((message) => {
2214
+ if (message.role === "user") question2 = textOfMessage(message);
2215
+ if (message.role !== "assistant") return message;
2216
+ const cut = cutForgedBlocksInParts(message.parts, formats, { explaining: explainsServiceFormats(question2, formats) });
2217
+ if (!cut) return message;
2218
+ changed = true;
2219
+ return { ...message, parts: cut.parts };
2220
+ });
2221
+ return changed ? cleaned : history;
2222
+ }
2223
+ function createForgedBlockGuard(formats) {
2224
+ let worded = false;
2225
+ let explaining = false;
2226
+ const cut = [];
2227
+ const kept = [];
2228
+ const transformFor = () => {
2229
+ const scanners = /* @__PURE__ */ new Map();
2230
+ let held = null;
2231
+ const starts = /* @__PURE__ */ new Map();
2232
+ const started = /* @__PURE__ */ new Set();
2233
+ const enqueue = (part, controller) => {
2234
+ if (part.type === "text-start" && scanners.has(part.id)) {
2235
+ starts.set(part.id, part);
2236
+ return;
2237
+ }
2238
+ if (part.type === "text-delta" && !started.has(part.id) && starts.has(part.id)) {
2239
+ controller.enqueue(starts.get(part.id));
2240
+ started.add(part.id);
2241
+ }
2242
+ if (part.type === "text-end" && starts.has(part.id) && !started.has(part.id)) return;
2243
+ controller.enqueue(part);
2244
+ };
2245
+ const isBlock = (item) => "name" in item && "text" in item && !("type" in item);
2246
+ const settle = (keep, controller) => {
2247
+ const items = held ?? [];
2248
+ held = null;
2249
+ for (const item of items) {
2250
+ if (!isBlock(item)) enqueue(item, controller);
2251
+ else {
2252
+ if (keep) enqueue({ type: "text-delta", id: item.id, delta: item.text }, controller);
2253
+ if (item.name) (keep ? kept : cut).push(item.name);
2254
+ }
2255
+ }
2256
+ };
2257
+ const send = (part, controller) => {
2258
+ if (held) held.push(part);
2259
+ else enqueue(part, controller);
2260
+ };
2261
+ const deltaOf = (id, pieces, controller) => {
2262
+ let delta = "";
2263
+ for (const piece of pieces) {
2264
+ if (piece.kind === "text" && hasWords(piece.text)) {
2265
+ worded = true;
2266
+ if (held) settle(false, controller);
2267
+ }
2268
+ if (piece.kind === "text" && !held) delta += piece.text;
2269
+ else if (worded) {
2270
+ if (piece.kind === "block") cut.push(piece.name);
2271
+ } else {
2272
+ (held ??= []).push({ id, name: piece.kind === "block" ? piece.name : "", text: piece.text });
2273
+ }
2274
+ }
2275
+ return delta;
2276
+ };
2277
+ const transform = (part, controller) => {
2278
+ switch (part.type) {
2279
+ case "text-start":
2280
+ scanners.set(part.id, createForgedBlockScanner(formats, { explaining }));
2281
+ break;
2282
+ case "text-delta": {
2283
+ const scanner = scanners.get(part.id);
2284
+ if (!scanner) break;
2285
+ const delta = deltaOf(part.id, scanner.push(part.delta), controller);
2286
+ if (delta) send({ ...part, delta }, controller);
2287
+ return;
2288
+ }
2289
+ case "text-end": {
2290
+ const scanner = scanners.get(part.id);
2291
+ if (!scanner) break;
2292
+ scanners.delete(part.id);
2293
+ const delta = deltaOf(part.id, scanner.end(), controller);
2294
+ if (delta) send({ type: "text-delta", id: part.id, delta }, controller);
2295
+ break;
2296
+ }
2297
+ case "tool-input-start":
2298
+ case "tool-call":
2299
+ if (held) settle(false, controller);
2300
+ break;
2301
+ case "finish":
2302
+ if (held) settle(part.finishReason.unified !== "tool-calls", controller);
2303
+ break;
2304
+ }
2305
+ send(part, controller);
2306
+ };
2307
+ const flush = (controller) => {
2308
+ if (held) settle(true, controller);
2309
+ };
2310
+ return new TransformStream({ transform, flush });
2311
+ };
2312
+ return {
2313
+ /**
2314
+ * Новая попытка с последним вопросом человека: шаги прежней стёрты — слов у ответа снова нет. Вырезанное раньше
2315
+ * остаётся в счёте для журнала.
2316
+ */
2317
+ reset(question2 = "") {
2318
+ worded = false;
2319
+ explaining = explainsServiceFormats(question2, formats);
2320
+ },
2321
+ /** Законченный текст мимо потока (переписка проверкой) — по тем же правилам, что поток этой попытки. */
2322
+ cutText: (text) => cutForgedBlocks(text, formats, { explaining }),
2323
+ /** Модель попытки под присмотром: каждый запрос — новый шаг. */
2324
+ wrap(model) {
2325
+ if (typeof model === "string" || formats.length === 0) return model;
2326
+ return wrapLanguageModel2({
2327
+ model,
2328
+ middleware: {
2329
+ specificationVersion: "v4",
2330
+ wrapStream: async ({ doStream }) => {
2331
+ const result = await doStream();
2332
+ return { ...result, stream: result.stream.pipeThrough(transformFor()) };
2333
+ }
2334
+ }
2335
+ });
2336
+ },
2337
+ /** Вырезанные блоки хода по порядку (имена) — для журнала. */
2338
+ cutBlocks: () => cut,
2339
+ /** Блоки, оставленные ради непустого ответа. */
2340
+ keptBlocks: () => kept
2341
+ };
2342
+ }
2343
+
1856
2344
  // src/storage/chat-storage.ts
1857
2345
  var RELEASED_ANSWER_ALIVE_AT = (/* @__PURE__ */ new Date(0)).toISOString();
1858
2346
  function aliveAtByLocalClock(mark) {
@@ -2918,7 +3406,7 @@ function usageMinusJournaled(usage, journaled) {
2918
3406
  }
2919
3407
 
2920
3408
  // src/agent-run/build-model-request.ts
2921
- import { convertToModelMessages, wrapLanguageModel as wrapLanguageModel4 } from "ai";
3409
+ import { convertToModelMessages, wrapLanguageModel as wrapLanguageModel5 } from "ai";
2922
3410
 
2923
3411
  // src/conversation/hanging-tool-calls.ts
2924
3412
  var PRELIMINARY_OUTPUT_MAX_LENGTH = 2e3;
@@ -3138,7 +3626,7 @@ ${chunk.delta}` } : chunk];
3138
3626
  }
3139
3627
 
3140
3628
  // src/agent-run/observe-model-calls.ts
3141
- import { wrapLanguageModel as wrapLanguageModel2 } from "ai";
3629
+ import { wrapLanguageModel as wrapLanguageModel3 } from "ai";
3142
3630
  function observeModelCalls(model, context) {
3143
3631
  if (typeof model === "string") return model;
3144
3632
  const now = context.now ?? Date.now;
@@ -3148,7 +3636,7 @@ function observeModelCalls(model, context) {
3148
3636
  attempts.set(params.prompt, attempt);
3149
3637
  return attempt;
3150
3638
  };
3151
- return wrapLanguageModel2({
3639
+ return wrapLanguageModel3({
3152
3640
  model,
3153
3641
  middleware: {
3154
3642
  specificationVersion: "v4",
@@ -3579,21 +4067,6 @@ function estimateMessagesTokens(messages) {
3579
4067
  }, 0);
3580
4068
  }
3581
4069
 
3582
- // src/agent-run/length-cut.ts
3583
- var LENGTH_CUT_PART_TYPE = "data-length-cut";
3584
- var LENGTH_CUT_NOTE = "[\u041E\u0442\u0432\u0435\u0442 \u043E\u0431\u043E\u0440\u0432\u0430\u043D: \u043C\u043E\u0434\u0435\u043B\u044C \u0443\u043F\u0451\u0440\u043B\u0430\u0441\u044C \u0432 \u043F\u0440\u0435\u0434\u0435\u043B \u0434\u043B\u0438\u043D\u044B \u043E\u0442\u0432\u0435\u0442\u0430]";
3585
- function lengthCutForModel(part) {
3586
- return part.type === LENGTH_CUT_PART_TYPE ? { type: "text", text: LENGTH_CUT_NOTE } : void 0;
3587
- }
3588
-
3589
- // src/agent-run/unanswered-messages.ts
3590
- var UNANSWERED_NOTE = "[\u041E\u0442\u0432\u0435\u0442\u0430 \u043D\u0430 \u044D\u0442\u043E \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043D\u0435\u0442: \u0445\u043E\u0434 \u043E\u0431\u043E\u0440\u0432\u0430\u043B\u0441\u044F \u0434\u043E \u043F\u0435\u0440\u0432\u044B\u0445 \u0441\u043B\u043E\u0432]";
3591
- function withUnansweredMarked(messages) {
3592
- return messages.flatMap(
3593
- (message, index) => message.role === "user" && messages[index - 1]?.role === "user" ? [{ role: "assistant", content: [{ type: "text", text: UNANSWERED_NOTE }] }, message] : [message]
3594
- );
3595
- }
3596
-
3597
4070
  // src/agent-run/staff-replies.ts
3598
4071
  function isStaffReply(message) {
3599
4072
  if (message.role !== "user") return false;
@@ -3751,7 +4224,7 @@ function withoutUndelivered(history) {
3751
4224
  }
3752
4225
 
3753
4226
  // src/agent-run/garbled-tail.ts
3754
- import { wrapLanguageModel as wrapLanguageModel3 } from "ai";
4227
+ import { wrapLanguageModel as wrapLanguageModel4 } from "ai";
3755
4228
  var SIGN_ORDER = ["think", "role", "restart", "tool"];
3756
4229
  var TOKEN_SOURCE = String.raw`<\/?think>|<\|[^|\n<>]{1,40}\|>|<|[^|\n<>]{1,40}|>`;
3757
4230
  var TEMPLATE_TOKEN = new RegExp(TOKEN_SOURCE, "g");
@@ -3923,7 +4396,7 @@ function createGarbledTailGuard() {
3923
4396
  /** Модель попытки под присмотром: каждый запрос — новый шаг. */
3924
4397
  wrap(model) {
3925
4398
  if (typeof model === "string") return model;
3926
- return wrapLanguageModel3({
4399
+ return wrapLanguageModel4({
3927
4400
  model,
3928
4401
  middleware: {
3929
4402
  specificationVersion: "v4",
@@ -4028,16 +4501,16 @@ function visibleBefore(earlier, visible, offset) {
4028
4501
  function withoutCodeAndQuotes(text) {
4029
4502
  let fence = null;
4030
4503
  return text.split("\n").map((line) => {
4031
- const opener = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
4032
4504
  if (fence !== null) {
4033
- if (opener && opener[0] === fence[0] && opener.length >= fence.length && line.trim() === opener) fence = null;
4505
+ if (closesCodeFence(line, fence)) fence = null;
4034
4506
  return " ".repeat(line.length);
4035
4507
  }
4508
+ const opener = codeFenceOf(line);
4036
4509
  if (opener) {
4037
4510
  fence = opener;
4038
4511
  return " ".repeat(line.length);
4039
4512
  }
4040
- if (/^(?: {4}|\t)/.test(line) || /^\s*>/.test(line)) return " ".repeat(line.length);
4513
+ if (isIndentedCodeOrQuote(line)) return " ".repeat(line.length);
4041
4514
  return line.replace(/(`+)[^`\n]*?\1/g, (code) => " ".repeat(code.length));
4042
4515
  }).join("\n");
4043
4516
  }
@@ -4065,7 +4538,8 @@ async function buildModelRequest(input) {
4065
4538
  ...input.repeatSilence && { repeatSilence: input.repeatSilence }
4066
4539
  });
4067
4540
  const tools = input.tools ? sortToolsByName(input.tools) : void 0;
4068
- const sendable = input.switchableTools ? withSwitchedOffCallsAsText(sendableHistory(input.history), input.switchableTools, tools) : sendableHistory(input.history);
4541
+ const cleaned = sendableHistory(input.serviceBlocks ? withoutForgedBlocks(input.history, input.serviceBlocks) : input.history);
4542
+ const sendable = input.switchableTools ? withSwitchedOffCallsAsText(cleaned, input.switchableTools, tools) : cleaned;
4069
4543
  const noted = withStaffRepliesJoined(withChannelNotes(withModeNotes(withBurstsJoined(withCorrectionsJoined(sendable)), input.modeNotes)));
4070
4544
  const held = input.frozenContext ? holdNoteAfterApprovals(withFrozenContext(noted, input.frozenContext)) : { messages: noted };
4071
4545
  const history = selectReasoningForModel(held.messages, {
@@ -4150,7 +4624,7 @@ function withoutLoneSurrogatesDeep(value) {
4150
4624
  }
4151
4625
  function withRequestWatch(model, provider, watch) {
4152
4626
  if (!watch || typeof model === "string") return model;
4153
- return wrapLanguageModel4({
4627
+ return wrapLanguageModel5({
4154
4628
  model,
4155
4629
  middleware: {
4156
4630
  specificationVersion: "v4",
@@ -4170,7 +4644,7 @@ function withRequestWatch(model, provider, watch) {
4170
4644
  });
4171
4645
  }
4172
4646
  function withTransientRetries(model, context) {
4173
- return typeof model === "string" ? model : wrapLanguageModel4({ model, middleware: createTransientRetryMiddleware(context) });
4647
+ return typeof model === "string" ? model : wrapLanguageModel5({ model, middleware: createTransientRetryMiddleware(context) });
4174
4648
  }
4175
4649
  function withLastMessageMark(messages, options2) {
4176
4650
  if (!options2 || messages.length === 0) return messages;
@@ -5970,6 +6444,7 @@ async function runTurn(turnInput, emitRaw) {
5970
6444
  );
5971
6445
  const turnUsage = createTurnUsage();
5972
6446
  const garbledTail = createGarbledTailGuard();
6447
+ const forgedBlocks = createForgedBlockGuard(turnInput.serviceBlocks ?? CORE_SERVICE_BLOCKS);
5973
6448
  const turnBudget = createTurnBudget(input.turnRules, { catalog: input.catalog, modelId: input.modelId, turnUsage });
5974
6449
  const messageUsage = () => {
5975
6450
  const spent = turnUsage.total();
@@ -6146,6 +6621,7 @@ async function runTurn(turnInput, emitRaw) {
6146
6621
  turnUsage,
6147
6622
  turnBudget,
6148
6623
  garbledTail,
6624
+ forgedBlocks,
6149
6625
  // Есть куда перейти при молчании — у того же провайдера оно не повторяется.
6150
6626
  repeatSilence: () => silenceTarget() === void 0,
6151
6627
  ...silenceSwitch.scope === "step" && switchedBySilence && {
@@ -6365,6 +6841,8 @@ async function runTurn(turnInput, emitRaw) {
6365
6841
  const garbled = cutGarbledTail(garbledTail.forCut(parts));
6366
6842
  const droppedCalls = garbledTail.droppedCalls();
6367
6843
  const garbleLog = `\u0447\u0430\u0442=${input.chatId} \u043E\u0442\u0432\u0435\u0442=${messageId} \u043C\u043E\u0434\u0435\u043B\u044C=${modelId} \u043F\u0440\u043E\u0432\u0430\u0439\u0434\u0435\u0440=${answeredProviderId}`;
6844
+ for (const name of new Set(forgedBlocks.cutBlocks())) logError(`[\u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0439 \u0431\u043B\u043E\u043A \u0432\u044B\u0440\u0435\u0437\u0430\u043D] \u0442\u0435\u0433=${name} ${garbleLog}`);
6845
+ for (const name of new Set(forgedBlocks.keptBlocks())) logError(`[\u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0439 \u0431\u043B\u043E\u043A \u043D\u0435 \u0432\u044B\u0440\u0435\u0437\u0430\u043D: \u043E\u0442\u0432\u0435\u0442 \u043E\u0441\u0442\u0430\u043B\u0441\u044F \u0431\u044B \u043F\u0443\u0441\u0442\u044B\u043C] \u0442\u0435\u0433=${name} ${garbleLog}`);
6368
6846
  if (garbled) {
6369
6847
  logError(`[\u0445\u0432\u043E\u0441\u0442 \u043E\u0431\u0440\u0435\u0437\u0430\u043D] ${garbleLog} \u043F\u0440\u0438\u0437\u043D\u0430\u043A\u0438=${garbled.signs.join(",")} \u0437\u043D\u0430\u043A\u043E\u0432=${garbled.removedChars} \u0432\u044B\u0437\u043E\u0432\u043E\u0432_\u043D\u0435_\u0437\u0430\u043F\u0443\u0449\u0435\u043D\u043E=${droppedCalls}`);
6370
6848
  const cut = garbled.parts.filter((part) => !garbledTail.isDroppedCall(part));
@@ -6376,7 +6854,7 @@ async function runTurn(turnInput, emitRaw) {
6376
6854
  const textIndex = lastTextIndex(parts);
6377
6855
  if (replyChecks && status === "complete" && textIndex !== -1 && !takeover.signal.aborted) {
6378
6856
  const original = parts[textIndex].text;
6379
- const text = await checkedReplyText({
6857
+ const checked2 = await checkedReplyText({
6380
6858
  checks: replyChecks.checks,
6381
6859
  text: original,
6382
6860
  ownerId: input.ownerId ?? "",
@@ -6386,6 +6864,7 @@ async function runTurn(turnInput, emitRaw) {
6386
6864
  rewrite: createReplyRewrite({ ...input, providerId: answeredProviderId, messageId, logError }),
6387
6865
  logError
6388
6866
  });
6867
+ const text = checked2 === original ? checked2 : withoutForgedBlocksInRewrite(forgedBlocks.cutText(checked2), checked2, (name) => logError(`[\u0441\u043B\u0443\u0436\u0435\u0431\u043D\u044B\u0439 \u0431\u043B\u043E\u043A \u0432\u044B\u0440\u0435\u0437\u0430\u043D] \u0442\u0435\u0433=${name} ${garbleLog}`));
6389
6868
  if (text !== original) {
6390
6869
  const corrected = withTextAt(parts, textIndex, text);
6391
6870
  if (heldText?.holdsText()) for (const chunk of heldText.release(text)) emitShown(chunk);
@@ -6643,6 +7122,7 @@ async function openAttempt(input, attempt, attemptSignal) {
6643
7122
  } = input;
6644
7123
  const request = await buildModelRequest({
6645
7124
  ...requestInput,
7125
+ serviceBlocks: input.serviceBlocks ?? CORE_SERVICE_BLOCKS,
6646
7126
  providerId,
6647
7127
  history: attempt.history,
6648
7128
  frozenContext: { current: { answerId: input.messageId, data: attempt.frozenContext }, group: Boolean(input.group) },
@@ -6685,9 +7165,11 @@ async function openAttempt(input, attempt, attemptSignal) {
6685
7165
  };
6686
7166
  const { noteAfterApprovals, ...modelRequest } = request;
6687
7167
  attempt.garbledTail.reset();
7168
+ attempt.forgedBlocks.reset(lastQuestionOf(attempt.history));
6688
7169
  const result = streamText({
6689
7170
  ...modelRequest,
6690
- model: attempt.garbledTail.wrap(modelRequest.model),
7171
+ // Сначала вырезка блоков, потом проверка на кашу: каша судит уже чистый текст.
7172
+ model: attempt.garbledTail.wrap(attempt.forgedBlocks.wrap(modelRequest.model)),
6691
7173
  ...approval.toolApproval && { toolApproval: gateToolApproval(approval.toolApproval, () => toolsClosed) },
6692
7174
  // Попытке — остаток шагов хода.
6693
7175
  // Помощник встал на подтверждении человека — ход ждёт, модель основного агента его незаконченным не видит.
@@ -7110,6 +7592,11 @@ function emitReply(text, emit) {
7110
7592
  emit({ type: "text-end", id });
7111
7593
  emit({ type: "finish-step" });
7112
7594
  }
7595
+ function withoutForgedBlocksInRewrite(cut, text, logCut) {
7596
+ if (cut.blocks.length === 0 || !/[\p{L}\p{N}]/u.test(cut.text)) return text;
7597
+ for (const name of new Set(cut.blocks)) logCut(name);
7598
+ return cut.text;
7599
+ }
7113
7600
 
7114
7601
  // src/helpers/run-helper.ts
7115
7602
  var DEFAULT_HELPER_MAX_STEPS = 10;
@@ -7755,6 +8242,59 @@ function sdkReasoningRequest(level) {
7755
8242
  }
7756
8243
  }
7757
8244
 
8245
+ // src/models/providers/tool-result-images.ts
8246
+ function withToolResultImages(given) {
8247
+ return (async (input, init) => {
8248
+ const fetchImplementation = given ?? globalThis.fetch;
8249
+ if (typeof init?.body !== "string" || !init.body.includes('"role":"tool"')) return fetchImplementation(input, init);
8250
+ let body;
8251
+ try {
8252
+ body = JSON.parse(init.body);
8253
+ } catch {
8254
+ return fetchImplementation(input, init);
8255
+ }
8256
+ let changed = false;
8257
+ for (const message of body.messages ?? []) {
8258
+ if (message.role !== "tool" || typeof message.content !== "string") continue;
8259
+ const parts = imageContent(message.content);
8260
+ if (!parts) continue;
8261
+ message.content = parts;
8262
+ changed = true;
8263
+ }
8264
+ return fetchImplementation(input, changed ? { ...init, body: JSON.stringify(body) } : init);
8265
+ });
8266
+ }
8267
+ function imageContent(text) {
8268
+ if (!text.startsWith("[")) return void 0;
8269
+ let value;
8270
+ try {
8271
+ value = JSON.parse(text);
8272
+ } catch {
8273
+ return void 0;
8274
+ }
8275
+ if (!Array.isArray(value)) return void 0;
8276
+ const parts = [];
8277
+ let images = 0;
8278
+ for (const part of value) {
8279
+ if (part?.type === "text" && typeof part.text === "string") {
8280
+ parts.push({ type: "text", text: part.text });
8281
+ continue;
8282
+ }
8283
+ const url = part?.type === "file" && typeof part.mediaType === "string" && part.mediaType.startsWith("image/") ? imageUrl(part.mediaType, part.data) : void 0;
8284
+ if (!url) return void 0;
8285
+ parts.push({ type: "image_url", image_url: { url } });
8286
+ images += 1;
8287
+ }
8288
+ return images > 0 ? parts : void 0;
8289
+ }
8290
+ function imageUrl(mediaType, data) {
8291
+ if (data?.type === "url" && typeof data.url === "string") return data.url;
8292
+ if (data?.type !== "data") return void 0;
8293
+ const raw = data.data;
8294
+ const base64 = typeof raw === "string" ? raw : raw && typeof raw === "object" ? Buffer.from(Object.keys(raw).sort((a, b) => Number(a) - Number(b)).map((key) => raw[key])).toString("base64") : void 0;
8295
+ return base64 === void 0 ? void 0 : `data:${mediaType === "image/jpg" ? "image/jpeg" : mediaType};base64,${base64}`;
8296
+ }
8297
+
7758
8298
  // src/models/providers/session-header.ts
7759
8299
  var SAFE_SESSION_ID = /^[A-Za-z0-9._:~-]{1,128}$/;
7760
8300
  function sessionHeaderValue(chatId) {
@@ -7780,7 +8320,7 @@ function createFireworksProvider(connectionOptions = {}) {
7780
8320
  id: "fireworks",
7781
8321
  displayName: "Fireworks",
7782
8322
  apiKeyEnv: "FIREWORKS_API_KEY",
7783
- createLanguageModel: (providerModelId, apiKey, options2) => createFireworks({ apiKey, fetch: options2?.fetch, ...connection })(providerModelId),
8323
+ createLanguageModel: (providerModelId, apiKey, options2) => createFireworks({ apiKey, fetch: withToolResultImages(options2?.fetch), ...connection })(providerModelId),
7784
8324
  reasoningRequest: (level) => sdkReasoningRequest(level),
7785
8325
  requestOptions: ({ chatId }) => ({
7786
8326
  providerOptions: {
@@ -7797,7 +8337,7 @@ var fireworksProvider = createFireworksProvider();
7797
8337
 
7798
8338
  // src/models/providers/openrouter.ts
7799
8339
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
7800
- import { wrapLanguageModel as wrapLanguageModel5 } from "ai";
8340
+ import { wrapLanguageModel as wrapLanguageModel6 } from "ai";
7801
8341
 
7802
8342
  // src/models/providers/openrouter-cache-mark-limit.ts
7803
8343
  function openrouterCacheMarkLimit(providerModelId) {
@@ -8047,7 +8587,7 @@ function createOpenRouterProvider(options2 = {}) {
8047
8587
  createLanguageModel: (providerModelId, apiKey, requestOptions) => {
8048
8588
  const markLimit = openrouterCacheMarkLimit(providerModelId);
8049
8589
  const fetch = markLimit === void 0 ? requestOptions?.fetch : fetchWithCacheMarkLimit(requestOptions?.fetch, markLimit);
8050
- return wrapLanguageModel5({
8590
+ return wrapLanguageModel6({
8051
8591
  model: createOpenRouter({ apiKey, fetch, ...connection }).chat(providerModelId, routingOptions),
8052
8592
  middleware: [
8053
8593
  openrouterRoutingMiddleware(routing),
@@ -8152,7 +8692,7 @@ function createTogetherProvider(connectionOptions = {}) {
8152
8692
  id: "together",
8153
8693
  displayName: "Together AI",
8154
8694
  apiKeyEnv: "TOGETHER_API_KEY",
8155
- createLanguageModel: (providerModelId, apiKey, options2) => createTogetherAI({ apiKey, fetch: options2?.fetch, ...connection })(providerModelId),
8695
+ createLanguageModel: (providerModelId, apiKey, options2) => createTogetherAI({ apiKey, fetch: withToolResultImages(options2?.fetch), ...connection })(providerModelId),
8156
8696
  reasoningRequest: (level) => sdkReasoningRequest(level),
8157
8697
  samplingRequest: ({ minP }) => minP === void 0 ? {} : { togetherai: { min_p: minP } },
8158
8698
  requestOptions: () => ({}),
@@ -8212,11 +8752,11 @@ var deepseek41FlashProfile = {
8212
8752
  displayName: "DeepSeek 4.1 Flash",
8213
8753
  contextWindowTokens: 1e6,
8214
8754
  acceptsImages: true,
8215
- // Together даёт модель только с текстом (каталог Together, 2026-10-11): картинка туда не уходит.
8216
- acceptsImagesByProvider: { together: false },
8217
- // Картинку в результате инструмента видит у самого DeepSeek (проверено вживую 2026-10-05) и через OpenRouter у всех
8218
- // хозяев, кроме запрещённых ниже: одни её не видят, другие отвергают запрос (замер всех хозяев модели в OpenRouter 2026-10-06, specs/research-openrouter-tool-images.md).
8219
- toolResultImageProviders: ["deepseek", "openrouter"],
8755
+ // Картинку в результате инструмента видит у самого DeepSeek (проверено вживую 2026-10-05), у Together и Fireworks
8756
+ // (вживую 2026-10-15: картинка в ответе инструмента и в сообщении человека — цвет назван верно; запись каталога
8757
+ // Together «только текст» устарела) и через OpenRouter у всех хозяев, кроме запрещённых ниже: одни её не видят,
8758
+ // другие отвергают запрос (замер всех хозяев модели в OpenRouter 2026-10-06, specs/research-openrouter-tool-images.md).
8759
+ toolResultImageProviders: ["deepseek", "together", "fireworks", "openrouter"],
8220
8760
  reasoning: { replayPastTurns: true, levels: ["off", "on"], defaultLevel: "on" },
8221
8761
  // Запрет стоит всегда, а не только при картинке: запрос одинаков в каждом ходе, кэш и выбранный хозяин не теряются.
8222
8762
  providerOptions: { openrouter: { provider: { ignore: ["deepinfra", "open-inference", "phala", "sail-research", "venice"] } } },
@@ -9949,7 +10489,7 @@ function isPlainObject3(value) {
9949
10489
  }
9950
10490
 
9951
10491
  // src/memory/memory-after-answer.ts
9952
- import { wrapLanguageModel as wrapLanguageModel6 } from "ai";
10492
+ import { wrapLanguageModel as wrapLanguageModel7 } from "ai";
9953
10493
  var DRAIN_WAIT_MS = 5e3;
9954
10494
  function createMemoryAfterAnswer({ memory, storage, catalog, env, logError, turnObserver }) {
9955
10495
  const running = /* @__PURE__ */ new Set();
@@ -9994,7 +10534,7 @@ function createMemoryAfterAnswer({ memory, storage, catalog, env, logError, turn
9994
10534
  };
9995
10535
  }
9996
10536
  function withRetries(model, context) {
9997
- return typeof model === "string" ? model : wrapLanguageModel6({ model, middleware: createTransientRetryMiddleware(context) });
10537
+ return typeof model === "string" ? model : wrapLanguageModel7({ model, middleware: createTransientRetryMiddleware(context) });
9998
10538
  }
9999
10539
 
10000
10540
  // src/conversation/to-agent-chat-message.ts
@@ -10249,6 +10789,7 @@ function createCompactionService(options2) {
10249
10789
  ...observation(ownerId, answerId),
10250
10790
  history,
10251
10791
  ...options2.attachments && { attachments: options2.attachments },
10792
+ ...options2.serviceBlocks && { serviceBlocks: options2.serviceBlocks },
10252
10793
  settings,
10253
10794
  logError,
10254
10795
  abortSignal: signal,
@@ -10392,6 +10933,7 @@ function createCompactionService(options2) {
10392
10933
  ...observation(ownerId, answerId),
10393
10934
  history: [...history, { id: `${chatId}-answer-steps`, role: "assistant", parts: steps }],
10394
10935
  ...options2.attachments && { attachments: options2.attachments },
10936
+ ...options2.serviceBlocks && { serviceBlocks: options2.serviceBlocks },
10395
10937
  settings,
10396
10938
  task: answerStepsTask(settings.summaryCeilingTokens),
10397
10939
  logError,
@@ -11252,6 +11794,13 @@ function createChatService(input) {
11252
11794
  const logError = input.logError ?? ((message) => console.error(message));
11253
11795
  const now = input.now ?? (() => /* @__PURE__ */ new Date());
11254
11796
  const replyChecks = checkedReplyChecks(input.replyChecks);
11797
+ const serviceBlocks = [
11798
+ ...CORE_SERVICE_BLOCKS,
11799
+ ...input.memory?.serviceBlocks ?? [],
11800
+ ...input.attachments?.serviceBlocks ?? [],
11801
+ ...input.agent.serviceBlocks ?? []
11802
+ ];
11803
+ checkServiceBlockFormats(serviceBlocks);
11255
11804
  const widgets = createAgentWidgets(input.agent.widgets);
11256
11805
  checkWidgetNamesFree(widgets, input.memory, input.background);
11257
11806
  const browserToolDescriptions = checkedBrowserTools(input.agent, input.memory, widgets);
@@ -11400,6 +11949,7 @@ function createChatService(input) {
11400
11949
  // Сжатие берёт кэш хода: у него те же инструменты, что у модели агента, — с браузерными и помощниками.
11401
11950
  agent: { ...options2.agent, tools: withBrowserDescriptions(options2.agent.tools, describedOnlyTools(browserToolDescriptions, options2.agent.helpers, toolResultLimit)) },
11402
11951
  ...options2.attachments && { attachments: options2.attachments },
11952
+ serviceBlocks,
11403
11953
  settings: resolveCompactionSettings(options2.compaction),
11404
11954
  logError,
11405
11955
  ...options2.turnObserver && { turnObserver: options2.turnObserver },
@@ -12063,6 +12613,7 @@ function createChatService(input) {
12063
12613
  browserTools: { descriptions: browserToolDescriptions, pending: pendingBrowserCalls, timeoutMs: browserToolTimeoutMs, tabId: launch.page?.tabId }
12064
12614
  },
12065
12615
  ...options2.attachments && { attachments: options2.attachments },
12616
+ serviceBlocks,
12066
12617
  ...options2.memory && { memory: options2.memory },
12067
12618
  ...widgets.waiting.size > 0 && { waitingWidgets: widgets.waiting },
12068
12619
  ...widgets.switchable.size > 0 && { switchableTools: widgets.switchable },
@@ -13515,6 +14066,10 @@ export {
13515
14066
  BURST_HOLD_MS,
13516
14067
  BURST_SWEEP_LIMIT,
13517
14068
  isBurstMessage,
14069
+ CORE_SERVICE_BLOCKS,
14070
+ serviceBlockName,
14071
+ checkServiceBlockFormats,
14072
+ cutForgedBlocks,
13518
14073
  RELEASED_ANSWER_ALIVE_AT,
13519
14074
  aliveAtByLocalClock,
13520
14075
  silenceBoundaryByLocalClock,
@@ -13614,4 +14169,4 @@ export {
13614
14169
  InvalidWidgetAnswerError,
13615
14170
  createChatService
13616
14171
  };
13617
- //# sourceMappingURL=chunk-KAG3QTZC.js.map
14172
+ //# sourceMappingURL=chunk-WQZQZCAB.js.map