@ykurilov/agent-chat-core 0.55.0 → 0.57.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +2 -0
  3. package/dist/access.js +2 -2
  4. package/dist/{chunk-PWW3R3VL.js → chunk-BS2HDKZB.js} +586 -41
  5. package/dist/chunk-BS2HDKZB.js.map +1 -0
  6. package/dist/{chunk-SNPF4BPB.js → chunk-FBULM4V6.js} +2 -2
  7. package/dist/{chunk-PWGI6NH6.js → chunk-FOAIG4DY.js} +1 -1
  8. package/dist/chunk-FOAIG4DY.js.map +1 -0
  9. package/dist/index.js +31 -3
  10. package/dist/index.js.map +1 -1
  11. package/dist/storage-contract.js +89 -7
  12. package/dist/storage-contract.js.map +1 -1
  13. package/dist/types/agent-run/agent-viewer.d.ts +60 -0
  14. package/dist/types/agent-run/garbled-tail.d.ts +69 -0
  15. package/dist/types/agent-run/reply-checks.d.ts +3 -1
  16. package/dist/types/agent-run/run-agent-turn.d.ts +8 -0
  17. package/dist/types/agent-run/staff-replies.d.ts +7 -0
  18. package/dist/types/bursts/message-bursts.d.ts +12 -0
  19. package/dist/types/chat-events/chat-events.d.ts +7 -0
  20. package/dist/types/chat-service.d.ts +29 -0
  21. package/dist/types/compaction/compaction-service.d.ts +3 -0
  22. package/dist/types/conversation/types.d.ts +3 -1
  23. package/dist/types/frozen-context/context-blocks.d.ts +3 -0
  24. package/dist/types/frozen-context/turn-context.d.ts +3 -0
  25. package/dist/types/index.d.ts +4 -2
  26. package/dist/types/storage/chat-storage.d.ts +47 -0
  27. package/dist/types/storage/storage-contract.d.ts +5 -0
  28. package/dist/types/turn-hooks/turn-gate.d.ts +2 -0
  29. package/dist/types/turn-hooks/turn-hooks.d.ts +7 -0
  30. package/package.json +1 -1
  31. package/dist/chunk-PWGI6NH6.js.map +0 -1
  32. package/dist/chunk-PWW3R3VL.js.map +0 -1
  33. /package/dist/{chunk-SNPF4BPB.js.map → chunk-FBULM4V6.js.map} +0 -0
@@ -3,7 +3,7 @@ import {
3
3
  TurnRefusedError,
4
4
  normalizeTurnRefusal,
5
5
  validateAppEvent
6
- } from "./chunk-PWGI6NH6.js";
6
+ } from "./chunk-FOAIG4DY.js";
7
7
 
8
8
  // src/conversation/types.ts
9
9
  var CURRENT_FORMAT_VERSION = 1;
@@ -41,7 +41,8 @@ var BUILT_IN_USAGE_SOURCES = [
41
41
  "judge",
42
42
  "first_message",
43
43
  "voice_input",
44
- "voice_polish"
44
+ "voice_polish",
45
+ "mcp_tool"
45
46
  ];
46
47
 
47
48
  // src/conversation/branches.ts
@@ -1717,6 +1718,9 @@ var DEFAULT_BURST_QUIET_MS = 3e3;
1717
1718
  var MAX_BURST_QUIET_MS = 6e5;
1718
1719
  var BURST_PENDING_GRACE_MS = 12e4;
1719
1720
  var BURST_BUSY_RETRY_MS = 1e3;
1721
+ var BURST_HOLD_MS = 3e4;
1722
+ var BURST_HOLD_RENEW_MS = 1e4;
1723
+ var BURST_SWEEP_LIMIT = 10;
1720
1724
  function burstPart(quietMs, at) {
1721
1725
  return { type: BURST_PART_TYPE, data: { quietMs, at } };
1722
1726
  }
@@ -1825,6 +1829,9 @@ function createBurstWaits() {
1825
1829
  wake(chatId) {
1826
1830
  for (const entry of [...timers]) if (entry.chatId === chatId) entry.done();
1827
1831
  },
1832
+ wakeAll() {
1833
+ for (const entry of [...timers]) entry.done();
1834
+ },
1828
1835
  has: (chatId) => waiting.has(chatId),
1829
1836
  async holding(chatId, work) {
1830
1837
  enter(chatId);
@@ -2284,6 +2291,57 @@ function jsonBytes(value) {
2284
2291
  }
2285
2292
  }
2286
2293
 
2294
+ // src/agent-run/agent-viewer.ts
2295
+ var AgentViewerError = class extends Error {
2296
+ constructor(message, options2) {
2297
+ super(message, options2);
2298
+ this.name = "AgentViewerError";
2299
+ }
2300
+ };
2301
+ async function resolveAgentViewer(source, input) {
2302
+ input.abortSignal.throwIfAborted();
2303
+ let viewer;
2304
+ try {
2305
+ viewer = await Promise.race([
2306
+ Promise.resolve().then(() => source(input)),
2307
+ new Promise((_, reject) => input.abortSignal.addEventListener("abort", () => reject(input.abortSignal.reason), { once: true }))
2308
+ ]);
2309
+ } catch (error) {
2310
+ if (input.abortSignal.aborted) throw error;
2311
+ throw new AgentViewerError(`agent.viewer \u0431\u0440\u043E\u0441\u0438\u043B\u0430: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
2312
+ }
2313
+ if (typeof viewer !== "object" || viewer === null || Array.isArray(viewer)) throw new AgentViewerError("agent.viewer \u0432\u0435\u0440\u043D\u0443\u043B\u0430 \u043D\u0435 \u043E\u0431\u044A\u0435\u043A\u0442");
2314
+ return Object.freeze({ ...viewer });
2315
+ }
2316
+ function viewerInput(turn) {
2317
+ const ownerId = turn.ownerId ?? "";
2318
+ return {
2319
+ userId: turn.staff?.id ?? ownerId,
2320
+ chatId: turn.chatId,
2321
+ ownerId,
2322
+ messageId: turn.messageId,
2323
+ chat: structuredClone({ ...turn.chatData && { data: turn.chatData } }),
2324
+ ...turn.subject && { subject: structuredClone(turn.subject) },
2325
+ channel: turn.channel ?? "web",
2326
+ audience: turn.audience,
2327
+ ...turn.staff && { staff: structuredClone(turn.staff) },
2328
+ abortSignal: turn.abortSignal
2329
+ };
2330
+ }
2331
+ var VIEWER_OUTSIDE_TURN_TIMEOUT_MS = 1e4;
2332
+ async function resolveViewerOutsideTurn(source, turn, signal) {
2333
+ const deadline = new AbortController();
2334
+ const timer = setTimeout(
2335
+ () => deadline.abort(new AgentViewerError(`agent.viewer \u043D\u0435 \u043E\u0442\u0432\u0435\u0442\u0438\u043B\u0430 \u0437\u0430 ${VIEWER_OUTSIDE_TURN_TIMEOUT_MS / 1e3} \u0441`)),
2336
+ VIEWER_OUTSIDE_TURN_TIMEOUT_MS
2337
+ );
2338
+ try {
2339
+ return await resolveAgentViewer(source, viewerInput({ ...turn, abortSignal: signal ? AbortSignal.any([signal, deadline.signal]) : deadline.signal }));
2340
+ } finally {
2341
+ clearTimeout(timer);
2342
+ }
2343
+ }
2344
+
2287
2345
  // src/helpers/run-helper.ts
2288
2346
  import { generateText as generateText2, stepCountIs as stepCountIs2 } from "ai";
2289
2347
 
@@ -2807,7 +2865,7 @@ function usageMinusJournaled(usage, journaled) {
2807
2865
  }
2808
2866
 
2809
2867
  // src/agent-run/build-model-request.ts
2810
- import { convertToModelMessages, wrapLanguageModel as wrapLanguageModel3 } from "ai";
2868
+ import { convertToModelMessages, wrapLanguageModel as wrapLanguageModel4 } from "ai";
2811
2869
 
2812
2870
  // src/conversation/hanging-tool-calls.ts
2813
2871
  var PRELIMINARY_OUTPUT_MAX_LENGTH = 2e3;
@@ -3483,6 +3541,37 @@ function withUnansweredMarked(messages) {
3483
3541
  );
3484
3542
  }
3485
3543
 
3544
+ // src/agent-run/staff-replies.ts
3545
+ function isStaffReply(message) {
3546
+ if (message.role !== "user") return false;
3547
+ const notices = message.parts.flatMap(noticesOfPart);
3548
+ return notices.length > 0 && notices.every((notice) => notice.kind === "staff");
3549
+ }
3550
+ function staffPartsJoined(parts) {
3551
+ return parts.flatMap((part) => {
3552
+ if (part.type !== AGENT_NOTICE_PART_TYPE) return joinedParts([part]);
3553
+ const text = noticePartForModel(part);
3554
+ return text ? [{ type: "text", text: `
3555
+
3556
+ ${text.text}` }] : [];
3557
+ });
3558
+ }
3559
+ function withStaffRepliesJoined(history) {
3560
+ const joined = [];
3561
+ let afterStaff = false;
3562
+ let changed = false;
3563
+ for (const message of history) {
3564
+ const previous = joined.at(-1);
3565
+ const staff = isStaffReply(message);
3566
+ if (previous?.role === "user" && message.role === "user" && (staff || afterStaff)) {
3567
+ joined[joined.length - 1] = { ...previous, parts: [...previous.parts, ...staff ? staffPartsJoined(message.parts) : joinedParts(message.parts)] };
3568
+ changed = true;
3569
+ } else joined.push(message);
3570
+ afterStaff = staff;
3571
+ }
3572
+ return changed ? joined : history;
3573
+ }
3574
+
3486
3575
  // src/frozen-context/frozen-context-messages.ts
3487
3576
  function frozenContextBase(history) {
3488
3577
  for (let index = history.length - 1; index >= 0; index -= 1) {
@@ -3608,6 +3697,298 @@ function withoutUndelivered(history) {
3608
3697
  return dropped.size === 0 ? history : history.filter((_, index) => !dropped.has(index));
3609
3698
  }
3610
3699
 
3700
+ // src/agent-run/garbled-tail.ts
3701
+ import { wrapLanguageModel as wrapLanguageModel3 } from "ai";
3702
+ var SIGN_ORDER = ["think", "role", "restart", "tool"];
3703
+ var TOKEN_SOURCE = String.raw`<\/?think>|<\|[^|\n<>]{1,40}\|>|<|[^|\n<>]{1,40}|>`;
3704
+ var TEMPLATE_TOKEN = new RegExp(TOKEN_SOURCE, "g");
3705
+ var ROLE_LINE = /(?<=^|\n)(user|assistant)(?=[ \t]*(?:\n|$)|[А-ЯЁа-яё])/g;
3706
+ var TOKEN_ROLE_LINE = /(?<=^|\n)(?:(?:<\|im_start\|>|<\|start_header_id\|>)(?:user|assistant)\b|<\|(?:user|assistant)\|>|<|(?:User|Assistant)|>)/g;
3707
+ var ANCHOR_HINT = /(?:^|\n)(?:(?:user|assistant)(?:[ \t]*(?:\n|$)|[А-ЯЁа-яё])|(?:<\|im_start\|>|<\|start_header_id\|>)(?:user|assistant)|<\|(?:user|assistant)\|>|<|(?:User|Assistant)|>)/;
3708
+ var ROLE_TOKEN = /(?:<\|im_start\|>|<\|start_header_id\|>)(user|assistant)\b|<\|(user|assistant)\|>|<|(User|Assistant)|>/g;
3709
+ var GLUED_SENTENCES = /[а-яё]{2}[.!?…][А-ЯЁ][а-яё]/;
3710
+ var changesVerdict = (lines) => ANCHOR_HINT.test(lines) || GLUED_SENTENCES.test(lines) || new RegExp(TOKEN_SOURCE).test(lines);
3711
+ var FINISHED = /[.!?…»)"]$/;
3712
+ var TRAILING_TOKENS = new RegExp(String.raw`(?:\s|${TOKEN_SOURCE})+$`);
3713
+ function findGarbledTail(parts) {
3714
+ if (!parts.some((part) => part.type === "text" && ANCHOR_HINT.test(part.text))) return null;
3715
+ const steps = stepsOf(parts);
3716
+ const markersByStep = steps.map((step) => step.texts.flatMap((text, at) => markersIn(text, step.texts.slice(0, at))));
3717
+ const bareRoles = new Set(markersByStep.flat().flatMap((marker) => marker.bareRole ? [marker.bareRole.name] : []));
3718
+ const tokenRoles = new Set(
3719
+ steps.flatMap((step) => step.texts.flatMap((text) => [...text.visible.matchAll(ROLE_TOKEN)].map((match) => (match[1] ?? match[2] ?? match[3]).toLowerCase())))
3720
+ );
3721
+ const dialog = bareRoles.size > 1;
3722
+ const template = tokenRoles.size > 1;
3723
+ for (const [index, step] of steps.entries()) {
3724
+ const markers = markersByStep[index].filter((marker) => !(dialog && marker.bareRole) && !(template && marker.roleToken));
3725
+ const found = garbleInStep(parts, step, markers);
3726
+ if (found) return found;
3727
+ }
3728
+ return null;
3729
+ }
3730
+ function cutGarbledTail(parts) {
3731
+ const found = findGarbledTail(parts);
3732
+ if (!found) return null;
3733
+ const stepEnd = parts.findIndex((part, index) => index > found.partIndex && part.type === "step-start");
3734
+ const end = stepEnd === -1 ? parts.length : stepEnd;
3735
+ const cutPart = parts[found.partIndex];
3736
+ const answer = cutPart.text.slice(0, found.offset).replace(TRAILING_TOKENS, "");
3737
+ let removedChars = cutPart.text.length - answer.length;
3738
+ const kept = parts.slice(0, found.partIndex);
3739
+ if (answer) kept.push({ ...cutPart, text: answer, state: "done" });
3740
+ for (const part of parts.slice(found.partIndex + 1, end)) {
3741
+ if (part.type === "text") removedChars += part.text.length;
3742
+ else if (part.type === "reasoning") continue;
3743
+ else if (!(stepEnd === -1 && part.type === LENGTH_CUT_PART_TYPE)) kept.push(part);
3744
+ }
3745
+ kept.push(...parts.slice(end));
3746
+ if (!kept.some((part) => part.type === "text" && /[\p{L}\p{N}]/u.test(part.text.replace(TEMPLATE_TOKEN, "")))) return null;
3747
+ const textIndex = answer ? found.partIndex : parts.slice(0, found.partIndex).findLastIndex((part) => part.type === "text");
3748
+ return { parts: kept, signs: found.signs, removedChars, textIndex };
3749
+ }
3750
+ function withoutGarbledTails(history) {
3751
+ let changed = false;
3752
+ const cleaned = history.map((message) => {
3753
+ if (message.role !== "assistant") return message;
3754
+ const parts = rememberedCut(message);
3755
+ if (!parts) return message;
3756
+ changed = true;
3757
+ return { ...message, parts };
3758
+ });
3759
+ return changed ? cleaned : history;
3760
+ }
3761
+ var rememberedCuts = /* @__PURE__ */ new Map();
3762
+ var REMEMBERED_CUTS_LIMIT = 500;
3763
+ function rememberedCut(message) {
3764
+ if (!message.parts.some((part) => part.type === "text" && ANCHOR_HINT.test(part.text))) return null;
3765
+ const source = JSON.stringify(message.parts);
3766
+ const known = rememberedCuts.get(message.id);
3767
+ if (known?.source === source) return known.parts;
3768
+ const parts = cutGarbledTail(message.parts)?.parts ?? null;
3769
+ rememberedCuts.delete(message.id);
3770
+ rememberedCuts.set(message.id, { source, parts });
3771
+ if (rememberedCuts.size > REMEMBERED_CUTS_LIMIT) rememberedCuts.delete(rememberedCuts.keys().next().value);
3772
+ return parts;
3773
+ }
3774
+ var DROPPED_CALL = { type: "tool-dropped", toolCallId: "", state: "input-available", input: {} };
3775
+ function createGarbledTailGuard() {
3776
+ let parts = [];
3777
+ let garbled = false;
3778
+ let droppedCalls = 0;
3779
+ let hinted = false;
3780
+ const worthChecking = (lines) => {
3781
+ hinted ||= ANCHOR_HINT.test(lines);
3782
+ return hinted && changesVerdict(lines);
3783
+ };
3784
+ const textAt = /* @__PURE__ */ new Map();
3785
+ const open = /* @__PURE__ */ new Set();
3786
+ const droppedIds = /* @__PURE__ */ new Set();
3787
+ const garbledNow = (withCall) => {
3788
+ if (!hinted) return false;
3789
+ const known = parts.map(
3790
+ (part, index) => part.type === "text" && open.has(index) ? { ...part, text: part.text.slice(0, part.text.lastIndexOf("\n") + 1) } : part
3791
+ );
3792
+ return cutGarbledTail(withCall ? [...known, DROPPED_CALL] : known) !== null;
3793
+ };
3794
+ const transform = (part, controller) => {
3795
+ switch (part.type) {
3796
+ case "text-start": {
3797
+ const index = parts.push({ type: "text", text: "" }) - 1;
3798
+ textAt.set(part.id, index);
3799
+ open.add(index);
3800
+ break;
3801
+ }
3802
+ case "text-delta": {
3803
+ if (garbled) return;
3804
+ const index = textAt.get(part.id);
3805
+ if (index === void 0) break;
3806
+ const text = parts[index];
3807
+ const before = text.text.length;
3808
+ const lineStart = text.text.lastIndexOf("\n") + 1;
3809
+ text.text += part.delta;
3810
+ if (!part.delta.includes("\n")) break;
3811
+ if (worthChecking(text.text.slice(lineStart, text.text.lastIndexOf("\n") + 1)) && garbledNow(false)) {
3812
+ garbled = true;
3813
+ const keep = text.text.lastIndexOf("\n") + 1 - before;
3814
+ text.text = text.text.slice(0, before + Math.max(keep, 0));
3815
+ if (keep > 0) controller.enqueue({ ...part, delta: part.delta.slice(0, keep) });
3816
+ return;
3817
+ }
3818
+ break;
3819
+ }
3820
+ case "text-end": {
3821
+ const index = textAt.get(part.id);
3822
+ if (index === void 0 || garbled) break;
3823
+ open.delete(index);
3824
+ const { text } = parts[index];
3825
+ if (worthChecking(text.slice(text.lastIndexOf("\n") + 1)) && garbledNow(false)) garbled = true;
3826
+ break;
3827
+ }
3828
+ case "reasoning-delta":
3829
+ if (garbled) return;
3830
+ break;
3831
+ case "tool-input-start":
3832
+ case "tool-call": {
3833
+ const callId = part.type === "tool-call" ? part.toolCallId : part.id;
3834
+ if (droppedIds.has(callId)) return;
3835
+ const started = part.type === "tool-call" && parts.some((known) => known.toolCallId === callId);
3836
+ if (started) break;
3837
+ if (garbled || garbledNow(true)) {
3838
+ garbled = true;
3839
+ droppedIds.add(callId);
3840
+ droppedCalls += 1;
3841
+ return;
3842
+ }
3843
+ parts.push({ type: "tool-call", toolCallId: callId, state: "input-streaming" });
3844
+ break;
3845
+ }
3846
+ case "tool-input-delta":
3847
+ case "tool-input-end":
3848
+ if (droppedIds.has(part.id)) return;
3849
+ break;
3850
+ case "finish":
3851
+ if (droppedCalls > 0 && part.finishReason.unified === "tool-calls") {
3852
+ controller.enqueue({ ...part, finishReason: { ...part.finishReason, unified: "stop" } });
3853
+ return;
3854
+ }
3855
+ break;
3856
+ }
3857
+ controller.enqueue(part);
3858
+ };
3859
+ return {
3860
+ /** Новая попытка: шаги прежней стёрты — всё сначала. */
3861
+ reset() {
3862
+ parts = [];
3863
+ garbled = false;
3864
+ droppedCalls = 0;
3865
+ hinted = false;
3866
+ textAt.clear();
3867
+ open.clear();
3868
+ droppedIds.clear();
3869
+ },
3870
+ /** Модель попытки под присмотром: каждый запрос — новый шаг. */
3871
+ wrap(model) {
3872
+ if (typeof model === "string") return model;
3873
+ return wrapLanguageModel3({
3874
+ model,
3875
+ middleware: {
3876
+ specificationVersion: "v4",
3877
+ wrapStream: async ({ doStream }) => {
3878
+ const result = await doStream();
3879
+ parts.push({ type: "step-start" });
3880
+ return { ...result, stream: result.stream.pipeThrough(new TransformStream({ transform })) };
3881
+ }
3882
+ }
3883
+ });
3884
+ },
3885
+ /** Срыв уже замечен в этой попытке: нового шага не нужно. */
3886
+ garbled: () => garbled,
3887
+ /** Сколько вызовов отброшено до запуска. */
3888
+ droppedCalls: () => droppedCalls,
3889
+ /** Части ответа для правила при сохранении: отброшенный вызов — на своём месте, в конце. */
3890
+ forCut: (saved) => droppedCalls > 0 ? [...saved, DROPPED_CALL] : saved,
3891
+ /** Заглушка отброшенного вызова из `forCut`: в запись не идёт. */
3892
+ isDroppedCall: (part) => part === DROPPED_CALL
3893
+ };
3894
+ }
3895
+ var isToolPart6 = (part) => part.type.startsWith("tool-") || part.type === "dynamic-tool";
3896
+ function stepsOf(parts) {
3897
+ const steps = [];
3898
+ let current = { from: 0, to: 0, texts: [] };
3899
+ parts.forEach((part, partIndex) => {
3900
+ if (part.type === "step-start") {
3901
+ current.to = partIndex;
3902
+ steps.push(current);
3903
+ current = { from: partIndex, to: partIndex, texts: [] };
3904
+ } else if (part.type === "text") current.texts.push({ partIndex, visible: withoutCodeAndQuotes(part.text) });
3905
+ });
3906
+ current.to = parts.length;
3907
+ steps.push(current);
3908
+ return steps.filter((step) => step.texts.length > 0);
3909
+ }
3910
+ function garbleInStep(parts, step, markers) {
3911
+ const roles = markers.filter((marker) => marker.kind === "role");
3912
+ const role = roles.find((marker) => finishedBefore(step.texts, marker));
3913
+ if (!role) return null;
3914
+ const after = (marker) => byPlace(marker, role) > 0;
3915
+ const tokens2 = markers.filter((marker) => marker.kind !== "role");
3916
+ const gluedAfter = () => textAfter(step.texts, role).some((text) => GLUED_SENTENCES.test(text));
3917
+ const others = /* @__PURE__ */ new Set();
3918
+ if (role.roleToken) {
3919
+ if (tokens2.some((marker) => !after(marker))) others.add("think");
3920
+ if (roles.some(after) || gluedAfter()) others.add("restart");
3921
+ } else {
3922
+ if (tokens2.length > 0) others.add("think");
3923
+ if (roles.some(after)) others.add("restart");
3924
+ if (role.bareRole.glued) {
3925
+ if (gluedAfter()) others.add("restart");
3926
+ if (parts.slice(role.partIndex + 1, step.to).some(isToolPart6)) others.add("tool");
3927
+ }
3928
+ }
3929
+ if (others.size === 0) return null;
3930
+ const closeThinks = markers.filter((marker) => marker.kind === "close-think" && !after(marker));
3931
+ const anchor = closeThinks.length >= 2 ? closeThinks[0] : role;
3932
+ const signs2 = /* @__PURE__ */ new Set(["role", ...others]);
3933
+ return { partIndex: anchor.partIndex, offset: anchor.offset, signs: SIGN_ORDER.filter((sign) => signs2.has(sign)) };
3934
+ }
3935
+ var byPlace = (a, b) => a.partIndex - b.partIndex || a.offset - b.offset;
3936
+ function textAfter(texts, anchor) {
3937
+ return texts.flatMap(
3938
+ (text) => text.partIndex < anchor.partIndex ? [] : [text.partIndex === anchor.partIndex ? text.visible.slice(anchor.offset) : text.visible]
3939
+ );
3940
+ }
3941
+ function markersIn(text, earlier) {
3942
+ const { partIndex, visible } = text;
3943
+ const markers = [];
3944
+ const hasTextBefore = (offset) => visibleBefore(earlier, visible, offset) !== "";
3945
+ const roleTokens = /* @__PURE__ */ new Set();
3946
+ for (const match of visible.matchAll(TOKEN_ROLE_LINE)) {
3947
+ roleTokens.add(match.index);
3948
+ if (hasTextBefore(match.index)) markers.push({ partIndex, offset: match.index, kind: "role", roleToken: true });
3949
+ }
3950
+ for (const match of visible.matchAll(TEMPLATE_TOKEN)) {
3951
+ if (roleTokens.has(match.index) || !hasTextBefore(match.index)) continue;
3952
+ markers.push({ partIndex, offset: match.index, kind: match[0] === "</think>" ? "close-think" : "token" });
3953
+ }
3954
+ for (const match of visible.matchAll(ROLE_LINE)) {
3955
+ if (!hasTextBefore(match.index)) continue;
3956
+ const name = match[1];
3957
+ const glued = /[А-ЯЁа-яё]/.test(visible[match.index + name.length] ?? "");
3958
+ markers.push({ partIndex, offset: match.index, kind: "role", bareRole: { name, glued } });
3959
+ }
3960
+ return markers.sort(byPlace);
3961
+ }
3962
+ function finishedBefore(texts, marker) {
3963
+ const at = texts.findIndex((text) => text.partIndex === marker.partIndex);
3964
+ return FINISHED.test(visibleBefore(texts.slice(0, at), texts[at].visible, marker.offset));
3965
+ }
3966
+ function visibleBefore(earlier, visible, offset) {
3967
+ const own = visible.slice(0, offset).replace(TRAILING_TOKENS, "");
3968
+ if (own) return own;
3969
+ for (let index = earlier.length - 1; index >= 0; index -= 1) {
3970
+ const text = earlier[index].visible.replace(TRAILING_TOKENS, "");
3971
+ if (text) return text;
3972
+ }
3973
+ return "";
3974
+ }
3975
+ function withoutCodeAndQuotes(text) {
3976
+ let fence = null;
3977
+ return text.split("\n").map((line) => {
3978
+ const opener = /^ {0,3}(`{3,}|~{3,})/.exec(line)?.[1];
3979
+ if (fence !== null) {
3980
+ if (opener && opener[0] === fence[0] && opener.length >= fence.length && line.trim() === opener) fence = null;
3981
+ return " ".repeat(line.length);
3982
+ }
3983
+ if (opener) {
3984
+ fence = opener;
3985
+ return " ".repeat(line.length);
3986
+ }
3987
+ if (/^(?: {4}|\t)/.test(line) || /^\s*>/.test(line)) return " ".repeat(line.length);
3988
+ return line.replace(/(`+)[^`\n]*?\1/g, (code) => " ".repeat(code.length));
3989
+ }).join("\n");
3990
+ }
3991
+
3611
3992
  // src/agent-run/build-model-request.ts
3612
3993
  async function buildModelRequest(input) {
3613
3994
  const found = findModel(input.catalog, input.modelId, input.providerId);
@@ -3632,7 +4013,7 @@ async function buildModelRequest(input) {
3632
4013
  });
3633
4014
  const tools = input.tools ? sortToolsByName(input.tools) : void 0;
3634
4015
  const sendable = input.switchableTools ? withSwitchedOffCallsAsText(sendableHistory(input.history), input.switchableTools, tools) : sendableHistory(input.history);
3635
- const noted = withChannelNotes(withModeNotes(withBurstsJoined(withCorrectionsJoined(sendable)), input.modeNotes));
4016
+ const noted = withStaffRepliesJoined(withChannelNotes(withModeNotes(withBurstsJoined(withCorrectionsJoined(sendable)), input.modeNotes)));
3636
4017
  const held = input.frozenContext ? holdNoteAfterApprovals(withFrozenContext(noted, input.frozenContext)) : { messages: noted };
3637
4018
  const history = selectReasoningForModel(held.messages, {
3638
4019
  modelId: profile.id,
@@ -3668,7 +4049,7 @@ async function buildModelRequest(input) {
3668
4049
  };
3669
4050
  }
3670
4051
  function sendableHistory(history) {
3671
- return withoutUndelivered(history).filter((message) => (message.role !== "assistant" || hasAnswerContent(message.parts)) && !isStateMessage(message)).map(withAssistantFilesAsText).map((message) => {
4052
+ return withoutGarbledTails(withoutUndelivered(history)).filter((message) => (message.role !== "assistant" || hasAnswerContent(message.parts)) && !isStateMessage(message)).map(withAssistantFilesAsText).map((message) => {
3672
4053
  const closed = message.role === "assistant" ? closeHangingToolCalls(message.parts) : message.parts;
3673
4054
  const parts = withoutLoneSurrogatesInParts(closed);
3674
4055
  return parts === message.parts ? message : { ...message, parts };
@@ -3716,7 +4097,7 @@ function withoutLoneSurrogatesDeep(value) {
3716
4097
  }
3717
4098
  function withRequestWatch(model, provider, watch) {
3718
4099
  if (!watch || typeof model === "string") return model;
3719
- return wrapLanguageModel3({
4100
+ return wrapLanguageModel4({
3720
4101
  model,
3721
4102
  middleware: {
3722
4103
  specificationVersion: "v4",
@@ -3736,7 +4117,7 @@ function withRequestWatch(model, provider, watch) {
3736
4117
  });
3737
4118
  }
3738
4119
  function withTransientRetries(model, context) {
3739
- return typeof model === "string" ? model : wrapLanguageModel3({ model, middleware: createTransientRetryMiddleware(context) });
4120
+ return typeof model === "string" ? model : wrapLanguageModel4({ model, middleware: createTransientRetryMiddleware(context) });
3740
4121
  }
3741
4122
  function withLastMessageMark(messages, options2) {
3742
4123
  if (!options2 || messages.length === 0) return messages;
@@ -5000,6 +5381,10 @@ function withTextAt(parts, index, text) {
5000
5381
  function createShownSteps() {
5001
5382
  let steps = 0;
5002
5383
  let textSteps = [];
5384
+ const resetsToText = (fromEnd) => {
5385
+ const step = textSteps.at(-1 - fromEnd);
5386
+ return step === void 0 || step === 0 ? null : steps - step + 1;
5387
+ };
5003
5388
  return {
5004
5389
  observe(chunk) {
5005
5390
  if (chunk.type === "start-step") steps += 1;
@@ -5010,10 +5395,9 @@ function createShownSteps() {
5010
5395
  }
5011
5396
  },
5012
5397
  /** Сколько шагов стереть с конца, чтобы убрать шаг последнего текста; `null` — текста на экране нет. */
5013
- resetsToLastText() {
5014
- const last = textSteps.at(-1);
5015
- return last === void 0 || last === 0 ? null : steps - last + 1;
5016
- }
5398
+ resetsToLastText: () => resetsToText(0),
5399
+ /** То же для текста `fromEnd`-го с конца (0 — последний); `null` — такого текста на экране нет. */
5400
+ resetsToText
5017
5401
  };
5018
5402
  }
5019
5403
  function replacementChunks(corrected, index, resets, providerId) {
@@ -5436,6 +5820,7 @@ async function freezeTurnContext(input, turn, base, place) {
5436
5820
  channel: input.channel ?? "web",
5437
5821
  audience: input.group ? "group" : "private",
5438
5822
  ...input.staff && { staff: structuredClone(input.staff) },
5823
+ ...turn.viewer && { viewer: turn.viewer },
5439
5824
  modelId: input.modelId,
5440
5825
  providerId: input.providerId
5441
5826
  }
@@ -5478,6 +5863,7 @@ async function runTurn(turnInput, emitRaw) {
5478
5863
  const externalContent = createExternalContentWatch();
5479
5864
  const emitShown = (chunk) => {
5480
5865
  shownSteps?.observe(chunk);
5866
+ screenSteps.observe(chunk);
5481
5867
  emitRaw(chunk);
5482
5868
  if (turnInput.liveDemos && externalContent.observe(chunk)) emitRaw(externalContentChunk());
5483
5869
  };
@@ -5485,6 +5871,7 @@ async function runTurn(turnInput, emitRaw) {
5485
5871
  const seenTexts = replyChecks && createSeenTexts();
5486
5872
  const heldText = replyChecks && (replyChecks.web === "hold" || (turnInput.channel ?? "web") !== "web") ? createHeldReplyText() : void 0;
5487
5873
  const shownSteps = replyChecks && createShownSteps();
5874
+ const screenSteps = createShownSteps();
5488
5875
  const emitToClient = (chunk) => {
5489
5876
  for (const passed of heldText ? heldText.pass(chunk) : [chunk]) emitShown(passed);
5490
5877
  };
@@ -5519,6 +5906,7 @@ async function runTurn(turnInput, emitRaw) {
5519
5906
  }
5520
5907
  );
5521
5908
  const turnUsage = createTurnUsage();
5909
+ const garbledTail = createGarbledTailGuard();
5522
5910
  const turnBudget = createTurnBudget(input.turnRules, { catalog: input.catalog, modelId: input.modelId, turnUsage });
5523
5911
  const messageUsage = () => {
5524
5912
  const spent = turnUsage.total();
@@ -5577,10 +5965,12 @@ async function runTurn(turnInput, emitRaw) {
5577
5965
  let errorKind;
5578
5966
  let providerId = input.providerId;
5579
5967
  let answeredProviderId = input.providerId;
5968
+ let viewer;
5580
5969
  try {
5581
5970
  const { history: historySource } = input;
5582
5971
  const fullHistory = typeof historySource === "function" ? await historySource() : historySource;
5583
5972
  const branchState = input.appState ?? foldAppState(fullHistory);
5973
+ viewer = input.viewer && await resolveAgentViewer(input.viewer, viewerInput({ ...input, audience: input.group ? "group" : "private" }));
5584
5974
  const prepared = input.prepareTurn && await input.prepareTurn({ state: branchState, abortSignal: input.abortSignal });
5585
5975
  const turnNotices = prepared && prepared.notices.length > 0 ? prepared.notices : void 0;
5586
5976
  const stateBase = prepared?.state ?? branchState;
@@ -5596,10 +5986,11 @@ async function runTurn(turnInput, emitRaw) {
5596
5986
  const frozenOfTurn = await frozenContextForTurn(input, {
5597
5987
  history: answer.request().history,
5598
5988
  answer: resume?.message ?? fullHistory.find((message) => message.id === messageId),
5599
- state: publicAppState(appState.values())
5989
+ state: publicAppState(appState.values()),
5990
+ ...viewer && { viewer }
5600
5991
  });
5601
5992
  const frozen = turnNotices && frozenOfTurn.fresh ? { ...frozenOfTurn, data: { ...frozenOfTurn.data, notices: turnNotices } } : frozenOfTurn;
5602
- let resumeNote = resume && !resume.replay && !frozen.fresh ? await resumeNoteForTurn(input, { history: answer.request().history, answer: resume.message, state: publicAppState(appState.values()) }) : void 0;
5993
+ let resumeNote = resume && !resume.replay && !frozen.fresh ? await resumeNoteForTurn(input, { history: answer.request().history, answer: resume.message, state: publicAppState(appState.values()), ...viewer && { viewer } }) : void 0;
5603
5994
  if (resume && turnNotices && !frozen.fresh) {
5604
5995
  const seen = latestFrozenContextOf(resume.message.parts);
5605
5996
  if (seen) resumeNote = { ...resumeNote ?? { blocks: seen.blocks, sent: { changed: [], removed: [] }, base: messageId }, notices: turnNotices };
@@ -5629,7 +6020,7 @@ async function runTurn(turnInput, emitRaw) {
5629
6020
  if (resume && !resume.replay) {
5630
6021
  const continued = helpersToContinue(resume.message.parts);
5631
6022
  if (continued.length > 0) {
5632
- await continueHelpers(input, continued, { toolQueue, dataChanges, appEvents, chatClosure, appState, emit, logError });
6023
+ await continueHelpers(input, continued, { ...viewer && { viewer }, toolQueue, dataChanges, appEvents, chatClosure, appState, emit, logError });
5633
6024
  const parts2 = await saver.snapshot();
5634
6025
  answer.update(parts2, { modelId, providerId: answeredProviderId });
5635
6026
  mainLoopWaits = input.abortSignal.aborted || pendingHumanInput(parts2, input.waitingWidgets).length > 0;
@@ -5671,6 +6062,7 @@ async function runTurn(turnInput, emitRaw) {
5671
6062
  steps: { done: stepsDone, max: maxSteps, beforeResume: stepsBeforeResume },
5672
6063
  historyRewritten,
5673
6064
  frozenContext: frozen.data,
6065
+ ...viewer && { viewer },
5674
6066
  toolQueue,
5675
6067
  endTurns,
5676
6068
  browser,
@@ -5682,6 +6074,7 @@ async function runTurn(turnInput, emitRaw) {
5682
6074
  emit,
5683
6075
  turnUsage,
5684
6076
  turnBudget,
6077
+ garbledTail,
5685
6078
  // Есть куда перейти при молчании — у того же провайдера оно не повторяется.
5686
6079
  repeatSilence: () => silenceTarget() === void 0,
5687
6080
  ...silenceSwitch.scope === "step" && switchedBySilence && {
@@ -5853,7 +6246,8 @@ async function runTurn(turnInput, emitRaw) {
5853
6246
  }
5854
6247
  } catch (error2) {
5855
6248
  if (!input.abortSignal.aborted) {
5856
- errorKind = classifyProviderError(error2, void 0);
6249
+ if (error2 instanceof AgentViewerError) logError(`[agent.viewer] \u0447\u0430\u0442=${input.chatId} \u0445\u043E\u0434 \u043D\u0435 \u043D\u0430\u0447\u0430\u0442: ${error2.message}`);
6250
+ errorKind = error2 instanceof AgentViewerError ? "other" : classifyProviderError(error2, void 0);
5857
6251
  errors = [{ type: "error", errorText: errorKind }];
5858
6252
  }
5859
6253
  }
@@ -5884,6 +6278,15 @@ async function runTurn(turnInput, emitRaw) {
5884
6278
  const failed = input.abortSignal.aborted || errors.length > 0;
5885
6279
  const closing = !failed && pendingHumanInput(savedParts, input.waitingWidgets).length > 0 && (closure !== void 0 || await chatClosedNow());
5886
6280
  let parts = failed || closing ? closeHangingToolCalls(denyPendingApprovals(savedParts, INTERRUPTED_REASON, RESULT_UNKNOWN_REASON), { notStarted: toolQueue.notStarted }) : savedParts;
6281
+ const garbled = cutGarbledTail(garbledTail.forCut(parts));
6282
+ const droppedCalls = garbledTail.droppedCalls();
6283
+ 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}`;
6284
+ if (garbled) {
6285
+ 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}`);
6286
+ const cut = garbled.parts.filter((part) => !garbledTail.isDroppedCall(part));
6287
+ if (!takeover.signal.aborted) showCutAnswer(parts, cut, garbled.textIndex, { heldText, screenSteps, providerId: answeredProviderId, emit: emitShown });
6288
+ parts = cut;
6289
+ } else if (findGarbledTail(garbledTail.forCut(parts))) logError(`[\u0445\u0432\u043E\u0441\u0442 \u043D\u0435 \u043E\u0431\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] ${garbleLog}`);
5887
6290
  const status = input.abortSignal.aborted ? "stopped" : errors.length > 0 ? "error" : pendingHumanInput(parts, input.waitingWidgets).length > 0 ? "awaiting_approval" : "complete";
5888
6291
  if (endTurns.hasText()) logError(`[endTurn] \u0447\u0430\u0442=${input.chatId} \u0433\u043E\u0442\u043E\u0432\u044B\u0439 \u0442\u0435\u043A\u0441\u0442 \u043D\u0435 \u043F\u043E\u043A\u0430\u0437\u0430\u043D: \u0445\u043E\u0434 \u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u0441\u043E \u0441\u0442\u0430\u0442\u0443\u0441\u043E\u043C ${status}`);
5889
6292
  const textIndex = lastTextIndex(parts);
@@ -6197,8 +6600,10 @@ async function openAttempt(input, attempt, attemptSignal) {
6197
6600
  return true;
6198
6601
  };
6199
6602
  const { noteAfterApprovals, ...modelRequest } = request;
6603
+ attempt.garbledTail.reset();
6200
6604
  const result = streamText({
6201
6605
  ...modelRequest,
6606
+ model: attempt.garbledTail.wrap(modelRequest.model),
6202
6607
  ...approval.toolApproval && { toolApproval: gateToolApproval(approval.toolApproval, () => toolsClosed) },
6203
6608
  // Попытке — остаток шагов хода.
6204
6609
  // Помощник встал на подтверждении человека — ход ждёт, модель основного агента его незаконченным не видит.
@@ -6206,6 +6611,7 @@ async function openAttempt(input, attempt, attemptSignal) {
6206
6611
  // Инструмент кончил ход: без текста — стоп сразу; с текстом — ещё шаг подставной модели, его держит только помощник
6207
6612
  // на подтверждении (ход ждёт человека). Потолок шагов и прочие стопы готовый текст не теряют.
6208
6613
  stopWhen: async (options2) => {
6614
+ if (attempt.garbledTail.garbled()) return true;
6209
6615
  if (attempt.endTurns.requested()) {
6210
6616
  if (attempt.endTurns.hasText()) return helperAwaitsApproval(options2);
6211
6617
  attempt.endTurns.stopWithoutText();
@@ -6462,6 +6868,7 @@ function turnToolsModel(input, found, turn) {
6462
6868
  ...input.subject && { subject: input.subject },
6463
6869
  chat: { ...input.chatData && { data: input.chatData } },
6464
6870
  ...input.staff && { staff: input.staff },
6871
+ ...turn.viewer && { viewer: turn.viewer },
6465
6872
  ...input.closeChat && { closeChat: turn.chatClosure.request },
6466
6873
  // Инструменту — только чтение и запись: конец записи и служебные ключи — дело ядра.
6467
6874
  state: { get: turn.appState.get, set: turn.appState.set, unset: turn.appState.unset }
@@ -6540,6 +6947,19 @@ function withLastUserPart(history, part) {
6540
6947
  return history.map((message, position) => position === index ? { ...message, parts: [...message.parts, part] } : message);
6541
6948
  }
6542
6949
  var FINISH_RETRY_PAUSES_MS = [1e3, 3e3];
6950
+ function showCutAnswer(original, cut, anchor, screen) {
6951
+ if (anchor === -1) return;
6952
+ const textsAfter = original.slice(anchor + 1).filter((part) => part.type === "text").length;
6953
+ if (screen.heldText?.holdsText()) {
6954
+ if (textsAfter === 0 && lastTextIndex(cut) === anchor) {
6955
+ for (const chunk of screen.heldText.release(cut[anchor].text)) screen.emit(chunk);
6956
+ return;
6957
+ }
6958
+ for (const chunk of screen.heldText.release()) screen.emit(chunk);
6959
+ }
6960
+ const resets = screen.screenSteps.resetsToText(textsAfter);
6961
+ for (const chunk of resets && replacementChunks(cut, anchor, resets, screen.providerId) || []) screen.emit(chunk);
6962
+ }
6543
6963
  async function finishWithRetries(storage, messageId, final, logError) {
6544
6964
  for (let attempt = 0; ; attempt += 1) {
6545
6965
  let failure;
@@ -6892,6 +7312,7 @@ function createChatEvents(logError = () => {
6892
7312
  listeners.delete(listener);
6893
7313
  };
6894
7314
  },
7315
+ hasListeners: () => listeners.size > 0,
6895
7316
  emit(event) {
6896
7317
  for (const listener of [...listeners]) {
6897
7318
  try {
@@ -6974,9 +7395,9 @@ function editAnswerParts(parts, edit, uneditable) {
6974
7395
  return { ok: true, parts: textPlaced ? edited : [...edited, textPart] };
6975
7396
  }
6976
7397
  function toolCallIdOf(part) {
6977
- return isToolPart6(part) ? part.toolCallId : void 0;
7398
+ return isToolPart7(part) ? part.toolCallId : void 0;
6978
7399
  }
6979
- var isToolPart6 = (part) => part.type.startsWith("tool-") || part.type === "dynamic-tool";
7400
+ var isToolPart7 = (part) => part.type.startsWith("tool-") || part.type === "dynamic-tool";
6980
7401
  function toolNameOf2(part) {
6981
7402
  return part.type === "dynamic-tool" ? part.toolName : part.type.slice("tool-".length);
6982
7403
  }
@@ -7270,7 +7691,7 @@ var fireworksProvider = createFireworksProvider();
7270
7691
 
7271
7692
  // src/models/providers/openrouter.ts
7272
7693
  import { createOpenRouter } from "@openrouter/ai-sdk-provider";
7273
- import { wrapLanguageModel as wrapLanguageModel4 } from "ai";
7694
+ import { wrapLanguageModel as wrapLanguageModel5 } from "ai";
7274
7695
 
7275
7696
  // src/models/providers/openrouter-cache-mark-limit.ts
7276
7697
  function openrouterCacheMarkLimit(providerModelId) {
@@ -7520,7 +7941,7 @@ function createOpenRouterProvider(options2 = {}) {
7520
7941
  createLanguageModel: (providerModelId, apiKey, requestOptions) => {
7521
7942
  const markLimit = openrouterCacheMarkLimit(providerModelId);
7522
7943
  const fetch = markLimit === void 0 ? requestOptions?.fetch : fetchWithCacheMarkLimit(requestOptions?.fetch, markLimit);
7523
- return wrapLanguageModel4({
7944
+ return wrapLanguageModel5({
7524
7945
  model: createOpenRouter({ apiKey, fetch, ...connection }).chat(providerModelId, routingOptions),
7525
7946
  middleware: [
7526
7947
  openrouterRoutingMiddleware(routing),
@@ -9422,7 +9843,7 @@ function isPlainObject3(value) {
9422
9843
  }
9423
9844
 
9424
9845
  // src/memory/memory-after-answer.ts
9425
- import { wrapLanguageModel as wrapLanguageModel5 } from "ai";
9846
+ import { wrapLanguageModel as wrapLanguageModel6 } from "ai";
9426
9847
  var DRAIN_WAIT_MS = 5e3;
9427
9848
  function createMemoryAfterAnswer({ memory, storage, catalog, env, logError, turnObserver }) {
9428
9849
  const running = /* @__PURE__ */ new Set();
@@ -9467,7 +9888,7 @@ function createMemoryAfterAnswer({ memory, storage, catalog, env, logError, turn
9467
9888
  };
9468
9889
  }
9469
9890
  function withRetries(model, context) {
9470
- return typeof model === "string" ? model : wrapLanguageModel5({ model, middleware: createTransientRetryMiddleware(context) });
9891
+ return typeof model === "string" ? model : wrapLanguageModel6({ model, middleware: createTransientRetryMiddleware(context) });
9471
9892
  }
9472
9893
 
9473
9894
  // src/conversation/to-agent-chat-message.ts
@@ -9717,7 +10138,7 @@ function createCompactionService(options2) {
9717
10138
  chatId,
9718
10139
  model,
9719
10140
  instructions: await instructionsOf(chat),
9720
- tools: resolveTools(model, chat, chatId, ownerId),
10141
+ tools: await resolveTools(model, chat, chatId, ownerId, branch, signal),
9721
10142
  ...options2.switchableTools && { switchableTools: options2.switchableTools },
9722
10143
  ...observation(ownerId, answerId),
9723
10144
  history,
@@ -9764,19 +10185,26 @@ function createCompactionService(options2) {
9764
10185
  function observation(ownerId, answerId) {
9765
10186
  return options2.turnObserver ? { observe: { observer: options2.turnObserver, ownerId, messageId: answerId ?? null } } : {};
9766
10187
  }
9767
- function resolveTools(model, chat, chatId, ownerId) {
9768
- const { tools } = options2.agent;
10188
+ async function resolveTools(model, chat, chatId, ownerId, branch, signal) {
10189
+ const { tools, viewer: viewerSource } = options2.agent;
9769
10190
  if (typeof tools !== "function") return tools;
9770
10191
  const subject = chat ? subjectOfChat(chat) : void 0;
10192
+ const chatData = chat ? chatDataOfInput(chat) : void 0;
10193
+ const { channel, group } = turnChannelOf(branch);
10194
+ const audience = group ? "group" : "private";
10195
+ const viewer = viewerSource && await resolveViewerOutsideTurn(viewerSource, { chatId, ownerId, messageId: "", ...chatData && { chatData }, ...subject && { subject }, channel, audience }, signal);
9771
10196
  return tools({
9772
10197
  profile: findModel(catalog, model.modelId, model.providerId).profile,
9773
10198
  providerId: model.providerId,
9774
10199
  chatId,
9775
10200
  ownerId,
9776
10201
  place: null,
10202
+ channel,
10203
+ audience,
9777
10204
  notifyDataChanged: (change) => logError(`\u0418\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442 \u0441\u043E\u043E\u0431\u0449\u0438\u043B \u043E\u0431 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u0438\u0438 \u0434\u0430\u043D\u043D\u044B\u0445 \xAB${String(change?.tag)}\xBB \u0432\u043E \u0432\u0440\u0435\u043C\u044F \u0441\u0436\u0430\u0442\u0438\u044F \u0447\u0430\u0442\u0430 ${chatId}: \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u043D\u0435 \u043E\u0442\u043F\u0440\u0430\u0432\u043B\u0435\u043D\u043E`),
9778
10205
  ...subject && { subject },
9779
- chat: { ...chat && chatDataOfInput(chat) && { data: chatDataOfInput(chat) } }
10206
+ chat: { ...chatData && { data: chatData } },
10207
+ ...viewer && { viewer }
9780
10208
  });
9781
10209
  }
9782
10210
  return {
@@ -9853,7 +10281,7 @@ function createCompactionService(options2) {
9853
10281
  chatId,
9854
10282
  model,
9855
10283
  instructions: await instructionsOf(chat),
9856
- tools: resolveTools(model, chat, chatId, ownerId),
10284
+ tools: await resolveTools(model, chat, chatId, ownerId, history, abortSignal),
9857
10285
  ...options2.switchableTools && { switchableTools: options2.switchableTools },
9858
10286
  ...observation(ownerId, answerId),
9859
10287
  history: [...history, { id: `${chatId}-answer-steps`, role: "assistant", parts: steps }],
@@ -10182,6 +10610,7 @@ function createTurnGate(hooks) {
10182
10610
  ...input.text !== void 0 && { text: input.text },
10183
10611
  ...input.messageId !== void 0 && { messageId: input.messageId },
10184
10612
  ...input.burstMessageIds && { burstMessageIds: [...input.burstMessageIds] },
10613
+ ...input.unattended && { unattended: true },
10185
10614
  ...abandonedOf(input)
10186
10615
  });
10187
10616
  if (!decision) return TURN;
@@ -10214,6 +10643,7 @@ function createTurnGate(hooks) {
10214
10643
  ...input.messageId !== void 0 && { messageId: input.messageId },
10215
10644
  ...input.text !== void 0 && { text: input.text },
10216
10645
  ...input.burstMessageIds && { burstMessageIds: [...input.burstMessageIds] },
10646
+ ...input.unattended && { unattended: true },
10217
10647
  reason,
10218
10648
  ...abandonedOf(input)
10219
10649
  });
@@ -10604,6 +11034,7 @@ var InvalidWidgetAnswerError = class extends Error {
10604
11034
  this.name = "InvalidWidgetAnswerError";
10605
11035
  }
10606
11036
  };
11037
+ var FAR_FUTURE_ISO = "9999-12-31T23:59:59.999Z";
10607
11038
  var TurnEndedBeforeSubscribeError = class extends Error {
10608
11039
  constructor(chatId) {
10609
11040
  super(`\u0425\u043E\u0434 \u0432 \u0447\u0430\u0442\u0435 ${chatId} \u0437\u0430\u043A\u043E\u043D\u0447\u0438\u043B\u0441\u044F \u0434\u043E \u043F\u043E\u0434\u043F\u0438\u0441\u043A\u0438`);
@@ -10847,7 +11278,9 @@ function createChatService(input) {
10847
11278
  const steersInFlight = /* @__PURE__ */ new Map();
10848
11279
  const startingByChat = /* @__PURE__ */ new Map();
10849
11280
  const burstWaits = createBurstWaits();
10850
- const turnLeases = createChatTurnLeases({ storage, holder: crypto.randomUUID(), logError });
11281
+ const copyId = crypto.randomUUID();
11282
+ const turnLeases = createChatTurnLeases({ storage, holder: copyId, logError });
11283
+ const burstSweeps = /* @__PURE__ */ new Set();
10851
11284
  const burstWrites = /* @__PURE__ */ new Map();
10852
11285
  const chatToolQueues = /* @__PURE__ */ new Map();
10853
11286
  let staleSweep = null;
@@ -11595,7 +12028,15 @@ function createChatService(input) {
11595
12028
  steeringTurn = steering.begin(chatId, answerId);
11596
12029
  if (stopsWhileStarting.delete(chatId)) registry.abort(chatId);
11597
12030
  const turnEvent = { ownerId: chat.ownerId, chatId, messageId: answerId, trigger, channel };
11598
- chatEvents.emit({ type: "turn-started", ...turnEvent, ...launch.deliveredByApp && { deliveredByApp: true } });
12031
+ chatEvents.emit({
12032
+ type: "turn-started",
12033
+ ...turnEvent,
12034
+ ...launch.deliveredByApp && { deliveredByApp: true },
12035
+ ...launch.unattended && { unattended: true }
12036
+ });
12037
+ if (launch.unattended && chatEvents.hasListeners?.() === false) {
12038
+ logError(`\u0425\u043E\u0434 \u0431\u0435\u0437 \u0432\u044B\u0437\u043E\u0432\u0430 \u043D\u0430 \u043F\u0430\u0447\u043A\u0443 \u0447\u0430\u0442\u0430 ${chatId}: \u043D\u0430 \u0441\u043E\u0431\u044B\u0442\u0438\u044F \u0447\u0430\u0442\u0430 \u043D\u0438\u043A\u0442\u043E \u043D\u0435 \u043F\u043E\u0434\u043F\u0438\u0441\u0430\u043D \u2014 \u043E\u0442\u0432\u0435\u0442 \u0431\u0443\u0434\u0435\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0432 \u0438\u0441\u0442\u043E\u0440\u0438\u0438`);
12039
+ }
11599
12040
  launch.onStarted?.({ answerId, finished });
11600
12041
  liveAnswerIds.add(answerId);
11601
12042
  const slot = startingSlots.get(chatId);
@@ -11713,6 +12154,7 @@ function createChatService(input) {
11713
12154
  await assertStillOpen(chatId);
11714
12155
  const earlier = await storage.getMessage(message.id);
11715
12156
  if (earlier?.chatId === chatId && earlier.role === "user") return earlier;
12157
+ if (sendOptions?.answerAfterRestart) await savingPendingBurst(userId, chatId, message.id, now().getTime() + quietMs, sendOptions.page);
11716
12158
  const written = await writingHumanMessage(userId, chatId, await currentBranch(chatId), message, text, files, sendOptions, { quietMs });
11717
12159
  chatEvents.emit({ type: "message-added", ownerId: userId, chatId, messageId: written.id });
11718
12160
  return written;
@@ -11731,11 +12173,11 @@ function createChatService(input) {
11731
12173
  return true;
11732
12174
  };
11733
12175
  }
11734
- async function answeringBurst(userId, chatId, question2, sendOptions) {
12176
+ async function answeringBurst(userId, chatId, questionId, page, unattended = false) {
11735
12177
  const otherCopy = waitingForOtherCopy(chatId);
11736
12178
  for (; ; ) {
11737
12179
  if (closing) throw new ChatBusyError(chatId);
11738
- const verdict = burstVerdict(await currentBranch(chatId), question2.id, now().getTime());
12180
+ const verdict = burstVerdict(await currentBranch(chatId), questionId, now().getTime());
11739
12181
  if (verdict.kind === "skip") return null;
11740
12182
  if (verdict.kind === "wait") {
11741
12183
  if (closing) throw new ChatBusyError(chatId);
@@ -11752,16 +12194,20 @@ function createChatService(input) {
11752
12194
  const burstMessageIds = verdict.burst.map((message) => message.id);
11753
12195
  try {
11754
12196
  const stream = await startingTurn(
11755
- { userId, chat, action: "send", text: burstText(verdict.burst), messageId: question2.id, burstMessageIds },
12197
+ { userId, chat, action: "send", text: burstText(verdict.burst), messageId: questionId, burstMessageIds, ...unattended && { unattended: true } },
11756
12198
  async (pass) => {
11757
12199
  const branch = await currentBranch(chatId);
11758
- if (burstVerdict(branch, question2.id, now().getTime()).kind !== "start") return null;
11759
- if (pass.kind === "hold") return heldTurn(userId, chatId, question2.id);
12200
+ if (burstVerdict(branch, questionId, now().getTime()).kind !== "start") return null;
12201
+ if (pass.kind === "hold") return unattended ? (heldTurn(userId, chatId, questionId), "unattended") : heldTurn(userId, chatId, questionId);
11760
12202
  takeTurnSlot(userId, chatId);
11761
- return startAnswer("send", userId, chatId, branch, sendOptions?.page, void 0, void 0, replyOf(pass));
12203
+ if (!unattended) return startAnswer("send", userId, chatId, branch, page, void 0, void 0, replyOf(pass));
12204
+ const reply = replyOf(pass);
12205
+ await launchTurn(userId, chatId, branch, { parentId: branch.at(-1).id, page, action: "send", detached: true, unattended: true, ...reply !== void 0 && { reply } });
12206
+ return "unattended";
11762
12207
  },
11763
12208
  (result) => result === null ? "busy" : void 0
11764
12209
  );
12210
+ if (stream === "unattended") return null;
11765
12211
  return stream && viewerStream(userId, chat, {}, async () => stream);
11766
12212
  } catch (error) {
11767
12213
  if (error instanceof ChatBusyError && !closing && localTurnOf(chatId)) continue;
@@ -11770,6 +12216,85 @@ function createChatService(input) {
11770
12216
  }
11771
12217
  }
11772
12218
  }
12219
+ const keepsPendingBursts = Boolean(storage.savePendingBurst && storage.takeAbandonedBursts && storage.renewPendingBurst && storage.removePendingBurst);
12220
+ async function savingPendingBurst(ownerId, chatId, messageId, dueAt, page) {
12221
+ if (!keepsPendingBursts) return;
12222
+ await storage.savePendingBurst({
12223
+ chatId,
12224
+ ownerId,
12225
+ messageId,
12226
+ dueAt: new Date(dueAt).toISOString(),
12227
+ holder: copyId,
12228
+ heldUntil: new Date(dueAt + BURST_HOLD_MS).toISOString(),
12229
+ ...page && { page }
12230
+ });
12231
+ }
12232
+ function renewingBurstHold(chatId, messageId, dueAt) {
12233
+ const timer = setInterval(() => {
12234
+ const heldUntil = new Date(Math.max(now().getTime(), dueAt) + BURST_HOLD_MS).toISOString();
12235
+ storage.renewPendingBurst({ chatId, messageId, holder: copyId, heldUntil }).then(
12236
+ (held) => {
12237
+ if (!held) clearInterval(timer);
12238
+ },
12239
+ (error) => logError(`\u0423\u0434\u0435\u0440\u0436\u0430\u043D\u0438\u0435 \u043F\u0430\u0447\u043A\u0438 \u0447\u0430\u0442\u0430 ${chatId} \u043D\u0435 \u043F\u0440\u043E\u0434\u043B\u0438\u043B\u043E\u0441\u044C: ${error instanceof Error ? error.message : String(error)}`)
12240
+ );
12241
+ }, BURST_HOLD_RENEW_MS);
12242
+ timer.unref?.();
12243
+ return () => clearInterval(timer);
12244
+ }
12245
+ const burstWaitsForNextSweep = (error) => error instanceof ChatBusyError || error instanceof TooManyTurnsError;
12246
+ function answeringRecordedBurst(ownerId, chatId, questionId, page, burst) {
12247
+ const unattended = burst.kept && burst.unattended === true;
12248
+ if (!burst.kept || !keepsPendingBursts) return burstWaits.holding(chatId, () => answeringBurst(ownerId, chatId, questionId, page, unattended));
12249
+ const stopRenewing = renewingBurstHold(chatId, questionId, burst.dueAt);
12250
+ const answering = burstWaits.holding(chatId, () => answeringBurst(ownerId, chatId, questionId, page, unattended)).finally(stopRenewing);
12251
+ void answering.then(
12252
+ () => true,
12253
+ (error) => !burstWaitsForNextSweep(error)
12254
+ ).then((decided) => {
12255
+ if (decided)
12256
+ return storage.removePendingBurst(chatId, questionId).catch((error) => logError(`\u0417\u0430\u043F\u0438\u0441\u044C \u043F\u0430\u0447\u043A\u0438 \u0447\u0430\u0442\u0430 ${chatId} \u043D\u0435 \u0441\u043D\u044F\u043B\u0430\u0441\u044C: ${error instanceof Error ? error.message : String(error)}`));
12257
+ });
12258
+ return answering;
12259
+ }
12260
+ let burstDecisions = 0;
12261
+ const burstDecisionWaiters = /* @__PURE__ */ new Set();
12262
+ const burstDecisionFreed = () => new Promise((resolve) => burstDecisionWaiters.add(resolve));
12263
+ function releasingBurstDecision() {
12264
+ burstDecisions--;
12265
+ for (const waiter of [...burstDecisionWaiters]) {
12266
+ burstDecisionWaiters.delete(waiter);
12267
+ waiter();
12268
+ }
12269
+ }
12270
+ function reportingAbandonedBurst(chatId, error) {
12271
+ if (burstWaitsForNextSweep(error)) return;
12272
+ const why = error instanceof TurnRefusedError ? `\u043E\u0442\u043A\u0430\u0437 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u044F ${error.code}: ${error.message}` : error instanceof Error ? error.message : String(error);
12273
+ logError(`\u0425\u043E\u0434 \u043D\u0430 \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u0443\u044E \u043F\u0430\u0447\u043A\u0443 \u0447\u0430\u0442\u0430 ${chatId} \u043D\u0435 \u043D\u0430\u0447\u0430\u043B\u0441\u044F: ${why}`);
12274
+ }
12275
+ function sweepingBursts(take) {
12276
+ if (!keepsPendingBursts || closing) return Promise.resolve();
12277
+ const sweep = (async () => {
12278
+ for (; ; ) {
12279
+ if (closing) return;
12280
+ const free = BURST_SWEEP_LIMIT - burstDecisions;
12281
+ if (free <= 0) {
12282
+ await burstDecisionFreed();
12283
+ continue;
12284
+ }
12285
+ const heldUntil = new Date(now().getTime() + BURST_HOLD_MS).toISOString();
12286
+ const taken = await storage.takeAbandonedBursts({ ...take, holder: copyId, heldUntil, limit: free });
12287
+ if (taken.length === 0) return;
12288
+ for (const burst of taken) {
12289
+ if (burstWaits.has(burst.chatId)) continue;
12290
+ burstDecisions++;
12291
+ answeringRecordedBurst(burst.ownerId, burst.chatId, burst.messageId, burst.page, { kept: true, dueAt: Date.parse(burst.dueAt), unattended: true }).catch((error) => reportingAbandonedBurst(burst.chatId, error)).finally(releasingBurstDecision);
12292
+ }
12293
+ }
12294
+ })().catch((error) => logError(`\u041E\u0431\u0445\u043E\u0434 \u0436\u0434\u0443\u0449\u0438\u0445 \u043F\u0430\u0447\u0435\u043A \u043D\u0435 \u0443\u0434\u0430\u043B\u0441\u044F: ${error instanceof Error ? error.message : String(error)}`)).finally(() => burstSweeps.delete(sweep));
12295
+ burstSweeps.add(sweep);
12296
+ return sweep;
12297
+ }
11773
12298
  async function stoppingPendingBurst(userId, chatId) {
11774
12299
  if (!burstWaits.has(chatId) && !isBurstPending(await currentBranch(chatId), now().getTime())) return false;
11775
12300
  try {
@@ -12195,7 +12720,13 @@ function createChatService(input) {
12195
12720
  }).catch(() => {
12196
12721
  });
12197
12722
  const question2 = await recording;
12198
- const turn = burstWaits.holding(chatId, () => answeringBurst(userId, chatId, question2, burstOptions));
12723
+ const turn = answeringRecordedBurst(
12724
+ userId,
12725
+ chatId,
12726
+ question2.id,
12727
+ burstOptions?.page,
12728
+ burstOptions?.answerAfterRestart ? { kept: true, dueAt: Date.parse(question2.createdAt) + quietMs } : { kept: false }
12729
+ );
12199
12730
  turn.catch(() => {
12200
12731
  });
12201
12732
  return { messageId: question2.id, turn };
@@ -12480,9 +13011,15 @@ function createChatService(input) {
12480
13011
  },
12481
13012
  async recoverInterruptedAnswers(recoverOptions) {
12482
13013
  await afterTurn?.recoverPending(liveAnswerIds);
12483
- const [count] = await Promise.all([recoverAnswersOnStart(recoverOptions?.soleServer === true), options2.background?.recover()]);
13014
+ const soleServer = recoverOptions?.soleServer === true;
13015
+ const [count] = await Promise.all([recoverAnswersOnStart(soleServer), options2.background?.recover()]);
13016
+ await sweepingBursts(soleServer ? { abandonedBefore: FAR_FUTURE_ISO, exceptHolder: copyId } : { abandonedBefore: now().toISOString() });
12484
13017
  return count;
12485
13018
  },
13019
+ async tickBursts() {
13020
+ burstWaits.wakeAll();
13021
+ await sweepingBursts({ abandonedBefore: now().toISOString() });
13022
+ },
12486
13023
  async closeChat({ ownerId, chatId, code, message }) {
12487
13024
  const closed = normalizeChatClosure({ code, message });
12488
13025
  await changeClosure(await ownChat(ownerId, chatId), closed);
@@ -12510,6 +13047,7 @@ function createChatService(input) {
12510
13047
  burstWaits.cancelAll();
12511
13048
  try {
12512
13049
  await sweeping;
13050
+ while (burstSweeps.size > 0) await Promise.allSettled([...burstSweeps]);
12513
13051
  while (startingWork.size > 0) await Promise.allSettled([...startingWork]);
12514
13052
  const unfinished = await liveTurns.drain(waitForAnswersMs);
12515
13053
  await burstWaits.settled();
@@ -12548,7 +13086,7 @@ function createChatService(input) {
12548
13086
  const next = () => {
12549
13087
  if (sweep.stopped) return;
12550
13088
  sweep.timer = setTimeout(() => {
12551
- sweep.running = Promise.all([recoverStaleAnswers(), afterTurn?.recoverPending(liveAnswerIds)]).then(() => void 0).catch(
13089
+ sweep.running = Promise.all([recoverStaleAnswers(), afterTurn?.recoverPending(liveAnswerIds), sweepingBursts({ abandonedBefore: now().toISOString() })]).then(() => void 0).catch(
12552
13090
  (failure) => logError(`\u041F\u0440\u043E\u0432\u0435\u0440\u043A\u0430 \u0431\u0440\u043E\u0448\u0435\u043D\u043D\u044B\u0445 \u043E\u0442\u0432\u0435\u0442\u043E\u0432 \u043D\u0435 \u0443\u0434\u0430\u043B\u0430\u0441\u044C: ${failure instanceof Error ? failure.message : "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"}`)
12553
13091
  ).finally(() => {
12554
13092
  sweep.running = void 0;
@@ -12632,6 +13170,9 @@ function createChatService(input) {
12632
13170
  const choice = effectiveChoiceOfChat(options2.catalog, env, chat, defaultChoice);
12633
13171
  const found = findModel(options2.catalog, choice.modelId, choice.providerId);
12634
13172
  const subject = subjectOfChat(chat);
13173
+ const { channel, group } = turnChannelOf(await loadCurrentBranch(storage, chatId));
13174
+ const audience = group ? "group" : "private";
13175
+ const viewer = options2.agent.viewer && await resolveViewerOutsideTurn(options2.agent.viewer, { chatId, ownerId, messageId: "", chatData: chatDataOfInput(chat), subject, channel, audience });
12635
13176
  const model = {
12636
13177
  profile: found.profile,
12637
13178
  providerId: found.provider.id,
@@ -12641,11 +13182,12 @@ function createChatService(input) {
12641
13182
  // Вкладки у фоновой задачи нет: сообщать об изменённых данных некому.
12642
13183
  notifyDataChanged: () => {
12643
13184
  },
12644
- channel: "web",
12645
- audience: "private",
13185
+ channel,
13186
+ audience,
12646
13187
  messageId: "",
12647
13188
  ...subject && { subject },
12648
- chat: { ...chatDataOfInput(chat) && { data: chatDataOfInput(chat) } }
13189
+ chat: { ...chatDataOfInput(chat) && { data: chatDataOfInput(chat) } },
13190
+ ...viewer && { viewer }
12649
13191
  };
12650
13192
  return helperToolsModel(model, {
12651
13193
  storage,
@@ -12829,6 +13371,8 @@ export {
12829
13371
  isCorrection,
12830
13372
  BURST_PART_TYPE,
12831
13373
  DEFAULT_BURST_QUIET_MS,
13374
+ BURST_HOLD_MS,
13375
+ BURST_SWEEP_LIMIT,
12832
13376
  isBurstMessage,
12833
13377
  RELEASED_ANSWER_ALIVE_AT,
12834
13378
  aliveAtByLocalClock,
@@ -12856,6 +13400,7 @@ export {
12856
13400
  ChatClosedError,
12857
13401
  isScreenPlaceInput,
12858
13402
  isTabId,
13403
+ AgentViewerError,
12859
13404
  runHelper,
12860
13405
  MAX_SERVER_TURNS_IN_ROW,
12861
13406
  createChatEvents,
@@ -12928,4 +13473,4 @@ export {
12928
13473
  InvalidWidgetAnswerError,
12929
13474
  createChatService
12930
13475
  };
12931
- //# sourceMappingURL=chunk-PWW3R3VL.js.map
13476
+ //# sourceMappingURL=chunk-BS2HDKZB.js.map