Spaces:
Paused
Paused
File size: 5,636 Bytes
fb4d8fe | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | import type { OpenClawConfig } from "../../config/config.js";
import type { BlockStreamingCoalesceConfig } from "../../config/types.js";
import { getChannelDock } from "../../channels/dock.js";
import { normalizeChannelId } from "../../channels/plugins/index.js";
import { normalizeAccountId } from "../../routing/session-key.js";
import {
INTERNAL_MESSAGE_CHANNEL,
listDeliverableMessageChannels,
} from "../../utils/message-channel.js";
import { resolveTextChunkLimit, type TextChunkProvider } from "../chunk.js";
const DEFAULT_BLOCK_STREAM_MIN = 800;
const DEFAULT_BLOCK_STREAM_MAX = 1200;
const DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS = 1000;
const getBlockChunkProviders = () =>
new Set<TextChunkProvider>([...listDeliverableMessageChannels(), INTERNAL_MESSAGE_CHANNEL]);
function normalizeChunkProvider(provider?: string): TextChunkProvider | undefined {
if (!provider) {
return undefined;
}
const cleaned = provider.trim().toLowerCase();
return getBlockChunkProviders().has(cleaned as TextChunkProvider)
? (cleaned as TextChunkProvider)
: undefined;
}
type ProviderBlockStreamingConfig = {
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
accounts?: Record<string, { blockStreamingCoalesce?: BlockStreamingCoalesceConfig }>;
};
function resolveProviderBlockStreamingCoalesce(params: {
cfg: OpenClawConfig | undefined;
providerKey?: TextChunkProvider;
accountId?: string | null;
}): BlockStreamingCoalesceConfig | undefined {
const { cfg, providerKey, accountId } = params;
if (!cfg || !providerKey) {
return undefined;
}
const providerCfg = (cfg as Record<string, unknown>)[providerKey];
if (!providerCfg || typeof providerCfg !== "object") {
return undefined;
}
const normalizedAccountId = normalizeAccountId(accountId);
const typed = providerCfg as ProviderBlockStreamingConfig;
const accountCfg = typed.accounts?.[normalizedAccountId];
return accountCfg?.blockStreamingCoalesce ?? typed.blockStreamingCoalesce;
}
export type BlockStreamingCoalescing = {
minChars: number;
maxChars: number;
idleMs: number;
joiner: string;
};
export function resolveBlockStreamingChunking(
cfg: OpenClawConfig | undefined,
provider?: string,
accountId?: string | null,
): {
minChars: number;
maxChars: number;
breakPreference: "paragraph" | "newline" | "sentence";
} {
const providerKey = normalizeChunkProvider(provider);
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
const providerChunkLimit = providerId
? getChannelDock(providerId)?.outbound?.textChunkLimit
: undefined;
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, {
fallbackLimit: providerChunkLimit,
});
const chunkCfg = cfg?.agents?.defaults?.blockStreamingChunk;
// Note: chunkMode="newline" used to imply splitting on each newline, but outbound
// delivery now treats it as paragraph-aware chunking (only split on blank lines).
// Block streaming should follow the same rule, so we do NOT special-case newline
// mode here.
// (chunkMode no longer alters block streaming behavior)
const maxRequested = Math.max(1, Math.floor(chunkCfg?.maxChars ?? DEFAULT_BLOCK_STREAM_MAX));
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
const minFallback = DEFAULT_BLOCK_STREAM_MIN;
const minRequested = Math.max(1, Math.floor(chunkCfg?.minChars ?? minFallback));
const minChars = Math.min(minRequested, maxChars);
const breakPreference =
chunkCfg?.breakPreference === "newline" || chunkCfg?.breakPreference === "sentence"
? chunkCfg.breakPreference
: "paragraph";
return { minChars, maxChars, breakPreference };
}
export function resolveBlockStreamingCoalescing(
cfg: OpenClawConfig | undefined,
provider?: string,
accountId?: string | null,
chunking?: {
minChars: number;
maxChars: number;
breakPreference: "paragraph" | "newline" | "sentence";
},
): BlockStreamingCoalescing | undefined {
const providerKey = normalizeChunkProvider(provider);
// Note: chunkMode="newline" is paragraph-aware in outbound delivery (blank-line splits),
// so block streaming should not disable coalescing or flush per single newline.
const providerId = providerKey ? normalizeChannelId(providerKey) : null;
const providerChunkLimit = providerId
? getChannelDock(providerId)?.outbound?.textChunkLimit
: undefined;
const textLimit = resolveTextChunkLimit(cfg, providerKey, accountId, {
fallbackLimit: providerChunkLimit,
});
const providerDefaults = providerId
? getChannelDock(providerId)?.streaming?.blockStreamingCoalesceDefaults
: undefined;
const providerCfg = resolveProviderBlockStreamingCoalesce({
cfg,
providerKey,
accountId,
});
const coalesceCfg = providerCfg ?? cfg?.agents?.defaults?.blockStreamingCoalesce;
const minRequested = Math.max(
1,
Math.floor(
coalesceCfg?.minChars ??
providerDefaults?.minChars ??
chunking?.minChars ??
DEFAULT_BLOCK_STREAM_MIN,
),
);
const maxRequested = Math.max(1, Math.floor(coalesceCfg?.maxChars ?? textLimit));
const maxChars = Math.max(1, Math.min(maxRequested, textLimit));
const minChars = Math.min(minRequested, maxChars);
const idleMs = Math.max(
0,
Math.floor(
coalesceCfg?.idleMs ?? providerDefaults?.idleMs ?? DEFAULT_BLOCK_STREAM_COALESCE_IDLE_MS,
),
);
const preference = chunking?.breakPreference ?? "paragraph";
const joiner = preference === "sentence" ? " " : preference === "newline" ? "\n" : "\n\n";
return {
minChars,
maxChars,
idleMs,
joiner,
};
}
|