// src/chat/context.ts
function estimateTokens(message, options) {
  const ratio = options?.charsPerToken ?? 4;
  let charCount = 0;
  charCount += message.role.length + 4;
  for (const part of message.parts) {
    charCount += estimatePartChars(part);
  }
  return Math.ceil(charCount / ratio);
}
function estimatePartChars(part) {
  switch (part.type) {
    case "text":
      return part.text.length;
    case "reasoning":
      return part.text.length;
    case "tool_call":
      return JSON.stringify(part.args).length + part.name.length + 20 + (part.result !== void 0 ? JSON.stringify(part.result).length : 0);
    case "source":
      return (part.title?.length ?? 0) + part.url.length + 10;
    case "file":
      return part.name.length + part.data.length + 20;
  }
}
var ContextWindowManager = class {
  config;
  constructor(config) {
    this.config = {
      maxTokens: config.maxTokens,
      reservedTokens: config.reservedTokens ?? 0,
      strategy: config.strategy ?? "truncate-oldest",
      estimation: config.estimation,
      summarizer: config.summarizer
    };
  }
  /** Available token budget after reserving tokens */
  get availableBudget() {
    return Math.max(0, this.config.maxTokens - this.config.reservedTokens);
  }
  /**
   * Estimate tokens for a single message.
   * @param message - Message to estimate
   * @returns Estimated token count
   */
  estimateMessageTokens(message) {
    return estimateTokens(message, this.config.estimation);
  }
  /**
   * Fit messages within the token budget using the configured strategy.
   * @param messages - All messages to consider
   * @returns Result with fitted messages and metadata
   */
  fitMessages(messages) {
    if (messages.length === 0) {
      return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
    }
    const budget = this.availableBudget;
    const tokenCounts = messages.map((m) => this.estimateMessageTokens(m));
    const totalTokens = tokenCounts.reduce((a, b) => a + b, 0);
    if (totalTokens <= budget) {
      return {
        messages: [...messages],
        totalTokens,
        removedCount: 0,
        wasTruncated: false
      };
    }
    switch (this.config.strategy) {
      case "truncate-oldest":
        return this.truncateOldest(messages, tokenCounts, budget);
      case "sliding-window":
        return this.slidingWindow(messages, tokenCounts, budget);
      case "summarize-placeholder":
        return this.summarizePlaceholder(messages, tokenCounts, budget);
    }
  }
  /**
   * Async variant of fitMessages that supports async summarization.
   * When strategy is "summarize-placeholder" and a summarizer is configured,
   * calls the summarizer with removed messages and replaces the placeholder text.
   * Falls back to static placeholder if summarizer throws.
   * For other strategies, behaves identically to fitMessages().
   */
  async fitMessagesAsync(messages) {
    const result = this.fitMessages(messages);
    if (this.config.strategy !== "summarize-placeholder" || !result.wasTruncated || !this.config.summarizer) {
      return result;
    }
    const keptIds = new Set(result.messages.map((m) => m.id));
    const removed = messages.filter((m) => !keptIds.has(m.id));
    if (removed.length === 0) return result;
    let summaryText;
    try {
      summaryText = await this.config.summarizer(removed);
    } catch {
      return result;
    }
    const updatedMessages = result.messages.map((m) => {
      if (m.metadata?.isSummary === true) {
        return {
          ...m,
          parts: [{ type: "text", text: summaryText, status: "complete" }]
        };
      }
      return m;
    });
    return { ...result, messages: updatedMessages };
  }
  /**
   * Trim messages using real token usage data from the previous API call.
   * Uses average-based algorithm: `avgTokensPerMessage = lastPromptTokens / messageCount`.
   * Removes oldest non-system messages until freed budget brings usage under modelContextWindow.
   *
   * @param messages - All messages in the session
   * @param lastPromptTokens - Real prompt tokens from the last API response
   * @param modelContextWindow - Model's total context window size in tokens
   * @returns Result with fitted messages and metadata
   */
  fitMessagesWithUsage(messages, lastPromptTokens, modelContextWindow) {
    if (messages.length === 0) {
      return { messages: [], totalTokens: 0, removedCount: 0, wasTruncated: false };
    }
    const budget = modelContextWindow - this.config.reservedTokens;
    if (budget <= 0 || lastPromptTokens <= budget) {
      return {
        messages: [...messages],
        totalTokens: lastPromptTokens,
        removedCount: 0,
        wasTruncated: false
      };
    }
    const avgTokensPerMessage = lastPromptTokens / messages.length;
    const tokensToFree = lastPromptTokens - budget;
    const messagesToRemove = Math.ceil(tokensToFree / avgTokensPerMessage);
    const nonSystemIndices = [];
    for (let i = 0; i < messages.length; i++) {
      if (messages[i].role === "system") ; else {
        nonSystemIndices.push(i);
      }
    }
    const removableCount = Math.min(messagesToRemove, nonSystemIndices.length);
    const removedIndices = new Set(nonSystemIndices.slice(0, removableCount));
    const result = [];
    for (let i = 0; i < messages.length; i++) {
      if (!removedIndices.has(i)) {
        result.push(messages[i]);
      }
    }
    const estimatedTokens = Math.round(
      lastPromptTokens * (result.length / messages.length)
    );
    return {
      messages: result,
      totalTokens: estimatedTokens,
      removedCount: removableCount,
      wasTruncated: removableCount > 0
    };
  }
  /**
   * Truncate oldest: keeps system messages, removes oldest non-system messages first.
   * Always keeps the most recent user message.
   */
  truncateOldest(messages, tokenCounts, budget) {
    const systemIndices = [];
    const nonSystemIndices = [];
    for (let i = 0; i < messages.length; i++) {
      if (messages[i].role === "system") {
        systemIndices.push(i);
      } else {
        nonSystemIndices.push(i);
      }
    }
    let usedTokens = systemIndices.reduce(
      (sum, i) => sum + tokenCounts[i],
      0
    );
    const includedNonSystem = [];
    for (let i = nonSystemIndices.length - 1; i >= 0; i--) {
      const idx = nonSystemIndices[i];
      if (usedTokens + tokenCounts[idx] <= budget) {
        includedNonSystem.unshift(idx);
        usedTokens += tokenCounts[idx];
      }
    }
    const includedSet = /* @__PURE__ */ new Set([...systemIndices, ...includedNonSystem]);
    const result = [];
    let resultTokens = 0;
    for (let i = 0; i < messages.length; i++) {
      if (includedSet.has(i)) {
        result.push(messages[i]);
        resultTokens += tokenCounts[i];
      }
    }
    return {
      messages: result,
      totalTokens: resultTokens,
      removedCount: messages.length - result.length,
      wasTruncated: true
    };
  }
  /**
   * Sliding window: keeps the most recent messages that fit within budget.
   */
  slidingWindow(messages, tokenCounts, budget) {
    const result = [];
    let usedTokens = 0;
    for (let i = messages.length - 1; i >= 0; i--) {
      if (usedTokens + tokenCounts[i] <= budget) {
        result.unshift(messages[i]);
        usedTokens += tokenCounts[i];
      } else {
        break;
      }
    }
    return {
      messages: result,
      totalTokens: usedTokens,
      removedCount: messages.length - result.length,
      wasTruncated: true
    };
  }
  /**
   * Summarize placeholder: replaces truncated messages with a placeholder,
   * preserving system messages and recent context.
   */
  summarizePlaceholder(messages, tokenCounts, budget) {
    const systemMessages = [];
    const nonSystem = [];
    for (let i = 0; i < messages.length; i++) {
      if (messages[i].role === "system") {
        systemMessages.push({ msg: messages[i], tokens: tokenCounts[i] });
      } else {
        nonSystem.push({ msg: messages[i], tokens: tokenCounts[i], idx: i });
      }
    }
    let usedTokens = systemMessages.reduce((s, m) => s + m.tokens, 0);
    const placeholderTokens = 20;
    usedTokens += placeholderTokens;
    const recentKept = [];
    for (let i = nonSystem.length - 1; i >= 0; i--) {
      if (usedTokens + nonSystem[i].tokens <= budget) {
        recentKept.unshift(nonSystem[i]);
        usedTokens += nonSystem[i].tokens;
      } else {
        break;
      }
    }
    const removedCount = messages.length - systemMessages.length - recentKept.length;
    const result = [];
    for (const sm of systemMessages) {
      result.push(sm.msg);
    }
    if (removedCount > 0) {
      result.push({
        id: "context-placeholder",
        role: "system",
        parts: [{ type: "text", text: `[${removedCount} earlier message${removedCount === 1 ? "" : "s"} omitted for context window]`, status: "complete" }],
        metadata: { isSummary: true },
        createdAt: (/* @__PURE__ */ new Date()).toISOString(),
        status: "complete"
      });
    }
    for (const m of recentKept) {
      result.push(m.msg);
    }
    return {
      messages: result,
      totalTokens: usedTokens,
      removedCount,
      wasTruncated: true
    };
  }
};

export { ContextWindowManager, estimateTokens };
//# sourceMappingURL=context.js.map
//# sourceMappingURL=context.js.map