// 装配、截断与丢弃报告。
//
// 不变量:
// V13 `Plan::tokens_used <= limit`,除非 `overflow` 被显式置真并给出原因。
// V14 `Pinned` 段永远完整出现在结果里;装不下时要给出可操作的建议。
// V15 `Plan::render` 的输出是规范文本(稳定、可冻结、可 diff、无 Map 迭代序)。
// V16 报告里的计数器身份(`name` + `exact`)与实际计数来源一致,估算必须自曝。
// V17 池配额的取用、借用、借出逐池记账;借用顺序 = `FitConfig::pools` 的声明顺序。
///|
/// 被完整保留的一段。
pub(all) struct KeptSegment {
index : Int
role : String
priority : Priority
tokens : Int64
bytes : Bytes
} derive(Debug)
///|
/// 被截短的一段:只保留一部分字节,`tokens` 是**实际装入**的量。
pub(all) struct TruncatedSegment {
index : Int
role : String
priority : Priority
original_tokens : Int64
tokens : Int64
strategy : TruncateStrategy
/// 被省略的量(`original - tokens`);`Compress` 时同时也写在占位标记里。
omitted : Int64
bytes : Bytes
} derive(Debug)
///|
/// 被整段丢弃的一段,带**原因**(这是本层相对同类工具的核心增量)。
pub(all) struct DroppedSegment {
index : Int
role : String
priority : Priority
tokens : Int64
reason : String
} derive(Debug)
///|
/// 一个配额池的台账。只有 `FitConfig::pools` 里声明过的池才会出现。
///
/// `demand` 是该池全部段的原始量,`used` 是最终装入量;`borrowed` / `lent`
/// 是与其他池之间的借用流水。`Pinned` 段不可裁,所以 `used` 允许大于
/// `quota`(配额只约束可裁段),报告如实呈现。
pub(all) struct PoolUsage {
name : String
quota : Int64
demand : Int64
used : Int64
borrowed : Int64
lent : Int64
} derive(Debug)
///|
/// 装配计划。`tokens_used` 是**已装入窗口**的 token 数;
/// 被丢弃的段不计入,被截断的段只计装入的部分。
///
/// `overflow` 为真时 `tokens_used > limit`,超出量在 `excess`;此时 `dropped`
/// 末尾还有一行**哨兵**(`index = -1`、`role = "*overflow*"`),它的 `tokens`
/// 是超出量而不是某个段的原量——按段对账时要跳过它。
pub(all) struct Plan {
/// 计数器身份(如 `cl100k_base` / `estimate@4`)
counter : String
/// 计数是否精确
exact : Bool
/// 窗口总大小
window : Int64
/// 留给模型回复的预留
reserve : Int64
/// 输入可用预算 `window - reserve`
limit : Int64
tokens_used : Int64
pinned_tokens : Int64
overflow : Bool
/// 溢出量 `tokens_used - limit`(不溢出时为 0)
excess : Int64
/// 池台账(按 `FitConfig::pools` 声明顺序;未配置池时为空)
pools : Array[PoolUsage]
kept : Array[KeptSegment]
truncated : Array[TruncatedSegment]
dropped : Array[DroppedSegment]
} derive(Debug)
///|
/// 按配置装配上下文。
///
/// 超出预算时的顺序完全确定:先动权重最小的段;权重相同时动**原始下标较小**的
/// (更早的历史)。策略为 `NoTruncate` 时整段丢弃;否则先尝试把该段截短到刚好够用,
/// 截不动(或目标为 0)时才整段丢。
pub fn fit_with(cfg : FitConfig, segments : Array[Segment]) -> Plan raise FitError {
if cfg.reserve_output >= cfg.window {
raise ReserveExceedsWindow(cfg.reserve_output, cfg.window)
}
assemble(cfg, segments)
}
///|
/// 装配但**不校验配置**:`reserve >= window`(甚至 `window <= 0`)时按实际预算如实记账。
///
/// 给 0.3 兼容入口 `tally.fit` 用——它必须保持旧语义:预算为 0 时全部丢弃、
/// Pinned 溢出上报,而不是抛错。普通调用请走 `fit_with`。
pub fn assemble(cfg : FitConfig, segments : Array[Segment]) -> Plan {
let limit = cfg.input_limit()
let counter = cfg.counter
let n = segments.length()
let counts : Array[Int64] = []
let original : Array[Int64] = []
// 截断段被省略的**原文**量(`Compress` 的标记里写的就是这个数)
let omitted_amounts : Array[Int64] = []
let pieces : Array[Bytes] = []
let dropped_flags : Array[Bool] = []
// 是否由「池配额」这一轮裁掉(决定报告里给出的原因)
let dropped_by_pool : Array[Bool] = []
let truncated_flags : Array[Bool] = []
// 段 → 声明池下标(-1 = 未声明,不参与配额)
let pool_idx : Array[Int] = []
let mut total : Int64 = 0
let mut pinned_tokens : Int64 = 0
for i = 0; i < n; i = i + 1 {
let s = segments[i]
let c = counter.count(s.bytes)
counts.push(c)
original.push(c)
omitted_amounts.push(0L)
pieces.push(s.bytes)
dropped_flags.push(false)
dropped_by_pool.push(false)
truncated_flags.push(false)
pool_idx.push(pool_index_of(cfg.pools, s.pool))
total = total + c
if s.priority == Pinned {
pinned_tokens = pinned_tokens + c
}
}
let mut overflow = false
// 第一轮:全局预算。
while total > limit {
let deficit = total - limit
let victim = pick_victim(segments, dropped_flags, -1, pool_idx)
if victim < 0 {
// 只剩 Pinned 还装不下:明确上报,而不是悄悄超限。
overflow = true
break
}
let c = counts[victim]
if cfg.strategy != NoTruncate && c > deficit {
// 截到「刚好补上缺口」的量,再用计数器回读实际装下的 token 数。
let (new_bytes, new_count, new_omitted) = shrink(
counter,
cfg.strategy,
pieces[victim],
c - deficit,
)
// `new_count == 0` 时等价于整段丢,但「丢掉」的解释力更强,故不记为截断。
if new_count < c && new_count > 0L {
pieces[victim] = new_bytes
counts[victim] = new_count
omitted_amounts[victim] = new_omitted
total = total - c + new_count
truncated_flags[victim] = true
continue
}
// 一个 token 都截不下来:退化为整段丢。
}
dropped_flags[victim] = true
total = total - c
}
// 第二轮:池配额(按声明顺序;借用也按声明顺序)。
let npools = cfg.pools.length()
let pool_used : Array[Int64] = Array::make(npools, 0L)
let pool_demand : Array[Int64] = Array::make(npools, 0L)
let pool_borrowed : Array[Int64] = Array::make(npools, 0L)
let pool_lent : Array[Int64] = Array::make(npools, 0L)
if npools > 0 {
for i = 0; i < n; i = i + 1 {
let p = pool_idx[i]
if p >= 0 {
pool_demand[p] = pool_demand[p] + original[i]
if !dropped_flags[i] {
pool_used[p] = pool_used[p] + counts[i]
}
}
}
for j = 0; j < npools; j = j + 1 {
let quota = cfg.pools[j].limit
let mut excess = pool_used[j] - quota
if excess <= 0L {
continue
}
// 借用:按声明顺序找有余量的池,借满为止。
for k = 0; k < npools; k = k + 1 {
if k == j || excess <= 0L {
continue
}
let spare = cfg.pools[k].limit - pool_used[k] - pool_lent[k]
if spare <= 0L {
continue
}
let take = if excess < spare { excess } else { spare }
// 借用只抬高借用方的可用额度,两池的「实际占用」都不变。
pool_borrowed[j] = pool_borrowed[j] + take
pool_lent[k] = pool_lent[k] + take
excess = excess - take
}
// 借完仍超额:只在该池内部继续裁。
while excess > 0L {
let victim = pick_victim(segments, dropped_flags, j, pool_idx)
if victim < 0 {
break
}
let c = counts[victim]
if cfg.strategy != NoTruncate && c > excess {
let (new_bytes, new_count, new_omitted) = shrink(
counter,
cfg.strategy,
pieces[victim],
c - excess,
)
if new_count < c && new_count > 0L {
pieces[victim] = new_bytes
counts[victim] = new_count
omitted_amounts[victim] = new_omitted
total = total - c + new_count
pool_used[j] = pool_used[j] - c + new_count
truncated_flags[victim] = true
excess = excess - (c - new_count)
continue
}
}
dropped_flags[victim] = true
dropped_by_pool[victim] = true
total = total - c
pool_used[j] = pool_used[j] - c
excess = excess - c
}
}
}
let kept : Array[KeptSegment] = []
let truncated : Array[TruncatedSegment] = []
let dropped : Array[DroppedSegment] = []
for i = 0; i < n; i = i + 1 {
let s = segments[i]
if dropped_flags[i] {
let p = pool_idx[i]
let reason = if p >= 0 && dropped_by_pool[i] {
pool_drop_reason(s.priority, cfg.pools[p].name, cfg.pools[p].limit)
} else {
drop_reason(s.priority)
}
dropped.push({
index: i,
role: s.role,
priority: s.priority,
tokens: original[i],
reason,
})
} else if truncated_flags[i] {
truncated.push({
index: i,
role: s.role,
priority: s.priority,
original_tokens: original[i],
tokens: counts[i],
strategy: cfg.strategy,
omitted: omitted_amounts[i],
bytes: pieces[i],
})
} else {
kept.push({
index: i,
role: s.role,
priority: s.priority,
tokens: counts[i],
bytes: pieces[i],
})
}
}
if overflow {
dropped.push({
index: -1,
role: "*overflow*",
priority: Pinned,
tokens: pinned_tokens - limit,
reason: "Pinned 段本身超过 limit;请增大窗口或缩短 system 提示/工具定义",
})
}
let pools : Array[PoolUsage] = []
for j = 0; j < npools; j = j + 1 {
pools.push({
name: cfg.pools[j].name,
quota: cfg.pools[j].limit,
demand: pool_demand[j],
used: pool_used[j],
borrowed: pool_borrowed[j],
lent: pool_lent[j],
})
}
{
counter: counter.name,
exact: counter.exact,
window: cfg.window,
reserve: cfg.reserve_output,
limit,
tokens_used: total,
pinned_tokens,
overflow,
excess: if overflow { total - limit } else { 0L },
pools,
kept,
truncated,
dropped,
}
}
///|
/// 段名 → 声明池下标;未声明返回 -1。
fn pool_index_of(pools : Array[PoolQuota], name : String) -> Int {
if name.length() == 0 {
return -1
}
for j = 0; j < pools.length(); j = j + 1 {
if pools[j].name == name {
return j
}
}
-1
}
///|
/// 选受害者:非 Pinned、未丢;权重最小 → 分数最小 → 下标最小。
/// `only_pool >= 0` 时只在该池内选(池配额裁剪)。
fn pick_victim(
segments : Array[Segment],
dropped_flags : Array[Bool],
only_pool : Int,
pool_idx : Array[Int],
) -> Int {
let mut victim = -1
let mut best_w = 5
let mut best_s = 0L
for i = 0; i < segments.length(); i = i + 1 {
if dropped_flags[i] || segments[i].priority == Pinned {
continue
}
if only_pool >= 0 && pool_idx[i] != only_pool {
continue
}
let w = segments[i].priority.weight()
let s = segments[i].score
if w < best_w || (w == best_w && s < best_s) {
victim = i
best_w = w
best_s = s
}
}
victim
}
///|
/// 把一段压到 `target` 个 token 以内,返回新字节与新计数。
/// 截不动(`target <= 0` 或一个 token 都留不下)时返回 `(空, 0)`,调用方据此整段丢。
fn shrink(
counter : Counter,
strategy : TruncateStrategy,
bytes : Bytes,
target : Int64,
) -> (Bytes, Int64, Int64) {
if target <= 0L {
return (slice_bytes(bytes, 0, 0), 0L, counter.count(bytes))
}
if strategy == Compress {
return compress_to(counter, bytes, target)
}
let (cut, count) = cut_to(counter, bytes, target, strategy == KeepSuffix)
(cut, count, counter.count(bytes) - count)
}
///|
/// 占位标记:固定文案,只带省略量(零依赖下这就是「占位压缩」的全部)。
fn compress_marker(omitted : Int64) -> Bytes {
@utf8.encode("\n…[tally: \{omitted} tokens omitted]…\n")
}
///|
/// 保留前缀,把尾部换成占位标记,并把总量压到 `target` 以内。
///
/// 标记长度随省略量的位数变化,所以先按上界留位,再用实际省略量收敛
/// (最多三轮);最后回读整段,保证报告里的 token 数与正文一致。
fn compress_to(
counter : Counter,
bytes : Bytes,
target : Int64,
) -> (Bytes, Int64, Int64) {
let original = counter.count(bytes)
if target <= 0L || original == 0L {
return (slice_bytes(bytes, 0, 0), 0L, original)
}
let mut marker = compress_marker(original)
let mut marker_count = counter.count(marker)
let keep_target = target - marker_count
if keep_target <= 0L {
return (slice_bytes(bytes, 0, 0), 0L, original)
}
let (prefix0, prefix0_count) = cut_to(counter, bytes, keep_target, false)
let mut prefix = prefix0
let mut prefix_count = prefix0_count
for k = 0; k < 3; k = k + 1 {
marker = compress_marker(original - prefix_count)
marker_count = counter.count(marker)
if prefix_count + marker_count <= target {
break
}
let retry = prefix_count - (prefix_count + marker_count - target)
if retry <= 0L {
return (slice_bytes(bytes, 0, 0), 0L, original)
}
let (p2, c2) = cut_to(counter, bytes, retry, false)
if c2 >= prefix_count {
break
}
prefix = p2
prefix_count = c2
}
let out = concat_bytes(prefix, marker)
// 拼接处可能产生新合并,token 数只会更少,不会更多。
(out, counter.count(out), original - prefix_count)
}
///|
fn concat_bytes(a : Bytes, b : Bytes) -> Bytes {
let out : Array[Byte] = []
for i = 0; i < a.length(); i = i + 1 {
out.push(a.unsafe_get(i))
}
for i = 0; i < b.length(); i = i + 1 {
out.push(b.unsafe_get(i))
}
Bytes::from_array(out)
}
///|
fn pool_drop_reason(p : Priority, pool : String, quota : Int64) -> String {
let why = match p {
Pinned => "不可丢但已溢出(不应发生)"
Newest => "池配额不足(最后才牺牲)"
Middle => "池配额不足(优先级低于 Newest)"
Oldest => "池配额不足(最早的历史,最先牺牲)"
}
"\{why}:池 \{pool} 配额 \{quota}"
}
///|
fn drop_reason(p : Priority) -> String {
match p {
Pinned => "不可丢但已溢出(不应发生)"
Newest => "为满足预算而丢弃(最后才牺牲)"
Middle => "为满足预算而丢弃(优先级低于 Newest)"
Oldest => "为满足预算而丢弃(最早的历史,最先牺牲)"
}
}
///|
/// 取 `bytes[from, to)` 的副本(`Bytes` 无 `op_set`,经 `Array[Byte]` 中转)。
fn slice_bytes(bytes : Bytes, from : Int, to : Int) -> Bytes {
let out : Array[Byte] = Array::make(to - from, (0).to_byte())
for i = 0; i < to - from; i = i + 1 {
out[i] = bytes.unsafe_get(from + i)
}
Bytes::from_array(out)
}
///|
/// 该字节是否是一个 UTF-8 字符的起始字节(非 `10xxxxxx` 续字节)。
fn is_char_start(b : Byte) -> Bool {
(b.to_int() & 0xC0) != 0x80
}
///|
/// 在 `bytes` 里取出「token 数不超过 `target`」的最大前缀(或后缀)切片。
///
/// 只依赖 `Counter`,因此对任意计数器(含外部 tokenizer)都成立。
/// 两处诚实说明:
/// * BPE 的 token 数对长度**不严格单调**(追加字节可能触发合并),二分只保证
/// 「找到的切片不超预算」,不保证是理论最长的那个;
/// * 切点会回退到 UTF-8 字符边界,避免把半个字符送给下游。
fn cut_to(
counter : Counter,
bytes : Bytes,
target : Int64,
suffix : Bool,
) -> (Bytes, Int64) {
let n = bytes.length()
if n == 0 || target <= 0L {
return (slice_bytes(bytes, 0, 0), 0L)
}
let mut lo = 0
let mut hi = n
// 记住 lo 对应的计数:若最后没有因 UTF-8 边界回退,`kept` 就是二分里
// 已经数过的那段,不必再数一遍(长段上省掉一次完整编码)。
let mut lo_count = 0L
while lo < hi {
let mid = (lo + hi + 1) / 2
let probe = if suffix {
slice_bytes(bytes, n - mid, n)
} else {
slice_bytes(bytes, 0, mid)
}
let c = counter.count(probe)
if c <= target {
lo = mid
lo_count = c
} else {
hi = mid - 1
}
}
let mut len = lo
while len > 0 {
let cut = if suffix { n - len } else { len }
if cut >= n || is_char_start(bytes.unsafe_get(cut)) {
break
}
len = len - 1
}
let kept = if suffix {
slice_bytes(bytes, n - len, n)
} else {
slice_bytes(bytes, 0, len)
}
let kept_count = if len == lo { lo_count } else { counter.count(kept) }
(kept, kept_count)
}