'use strict';

// src/chat/types.ts
function createChatId() {
  return crypto.randomUUID();
}
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
function toChatId(value) {
  if (!UUID_RE.test(value)) {
    throw new TypeError(`Invalid ChatId: "${value}" is not a valid UUID`);
  }
  return value;
}

// src/chat/bridge.ts
function chatEventToAgentEvent(event) {
  switch (event.type) {
    case "message:delta":
      return { type: "text_delta", text: event.text };
    case "thinking:start":
      return { type: "thinking_start" };
    case "thinking:delta":
      return { type: "thinking_delta", text: event.text };
    case "thinking:end":
      return { type: "thinking_end" };
    case "tool:start":
      return {
        type: "tool_call_start",
        toolCallId: event.toolCallId,
        toolName: event.toolName,
        args: event.args
      };
    case "tool:complete":
      return {
        type: "tool_call_end",
        toolCallId: event.toolCallId,
        toolName: event.toolName,
        result: event.result
      };
    case "error":
      return { type: "error", error: event.error, recoverable: event.recoverable, code: event.code };
    default:
      return null;
  }
}

// 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
    };
  }
};

// src/errors.ts
var AgentSDKError = class extends Error {
  /** @internal Marker for cross-bundle identity checks */
  _agentSDKError = true;
  /** Machine-readable error code. Prefer values from the ErrorCode enum. */
  code;
  /** Whether this error is safe to retry */
  retryable;
  /** HTTP status code hint for error classification */
  httpStatus;
  constructor(message, options) {
    super(message, options);
    this.name = "AgentSDKError";
    this.code = options?.code;
    this.retryable = options?.retryable ?? false;
    this.httpStatus = options?.httpStatus;
  }
  /** Check if an error is an AgentSDKError (works across bundled copies) */
  static is(error) {
    return error instanceof Error && "_agentSDKError" in error && error._agentSDKError === true;
  }
};

// src/chat/errors.ts
var ChatError = class extends AgentSDKError {
  code;
  retryable;
  retryAfter;
  timestamp;
  constructor(message, options) {
    super(message, {
      cause: options.cause,
      code: options.code,
      retryable: options.retryable
    });
    this.name = "ChatError";
    this.code = options.code;
    this.retryable = options.retryable ?? false;
    this.retryAfter = options.retryAfter;
    this.timestamp = (/* @__PURE__ */ new Date()).toISOString();
  }
};

// src/chat/state.ts
var StateMachine = class {
  constructor(initial, transitions) {
    this.initial = initial;
    this.transitions = transitions;
    this._current = initial;
  }
  _current;
  /** Current state */
  get current() {
    return this._current;
  }
  /**
   * Check whether transitioning to `next` is allowed from current state
   * @param next - Target state to check
   * @returns True if transition is allowed
   */
  canTransition(next) {
    const allowed = this.transitions[this._current];
    return allowed !== void 0 && allowed.includes(next);
  }
  /**
   * Transition to `next` state.
   * @throws ChatError(INVALID_TRANSITION) if the transition is not allowed
   */
  transition(next) {
    if (!this.canTransition(next)) {
      throw new ChatError(
        `Invalid transition: ${this._current} \u2192 ${next}`,
        { code: "INVALID_TRANSITION" /* INVALID_TRANSITION */ }
      );
    }
    this._current = next;
  }
  /** Reset to initial state */
  reset() {
    this._current = this.initial;
  }
};
var RUNTIME_TRANSITIONS = {
  idle: ["streaming", "disposed"],
  streaming: ["idle", "error", "disposed"],
  error: ["idle", "disposed"],
  disposed: []
};
var ChatReentrancyGuard = class {
  _acquired = false;
  /** Whether the guard is currently held */
  get isAcquired() {
    return this._acquired;
  }
  /**
   * Acquire the guard. Throws if already acquired.
   * @throws ChatError with code REENTRANCY
   */
  acquire() {
    if (this._acquired) {
      throw new ChatError(
        "Concurrent operation detected: a send is already in progress",
        { code: "REENTRANCY" /* REENTRANCY */ }
      );
    }
    this._acquired = true;
  }
  /** Release the guard. Safe to call even if not acquired. */
  release() {
    this._acquired = false;
  }
};
var ChatAbortController = class {
  _controller;
  _onExternalAbort;
  _externalSignal;
  constructor(externalSignal) {
    this._controller = new AbortController();
    this._externalSignal = externalSignal;
    if (externalSignal) {
      if (externalSignal.aborted) {
        this._controller.abort(externalSignal.reason);
      } else {
        this._onExternalAbort = () => {
          this._controller.abort(externalSignal.reason);
        };
        externalSignal.addEventListener("abort", this._onExternalAbort, { once: true });
      }
    }
  }
  /** The AbortSignal for this controller */
  get signal() {
    return this._controller.signal;
  }
  /** Whether the operation has been aborted */
  get isAborted() {
    return this._controller.signal.aborted;
  }
  /**
   * Abort the operation.
   * @param reason - Optional abort reason
   */
  abort(reason) {
    this._controller.abort(reason);
  }
  /** Clean up external signal listener to prevent memory leaks */
  dispose() {
    if (this._onExternalAbort && this._externalSignal) {
      this._externalSignal.removeEventListener("abort", this._onExternalAbort);
    }
  }
};

// src/chat/accumulator.ts
var MessageAccumulator = class {
  messageId;
  parts = [];
  status = "pending";
  currentTextPart = null;
  currentReasoningPart = null;
  toolCallParts = /* @__PURE__ */ new Map();
  _finalized = false;
  constructor(messageId) {
    this.messageId = messageId ?? createChatId();
  }
  /** Get current message ID */
  get id() {
    return this.messageId;
  }
  /**
   * Apply an AgentEvent to accumulate into the message
   * @param event - AgentEvent to process
   * @throws Error if accumulator is already finalized
   */
  apply(event) {
    if (this._finalized) throw new Error("Cannot apply events to finalized accumulator");
    if (this.status === "pending") {
      this.status = "streaming";
    }
    switch (event.type) {
      case "text_delta":
        this.handleTextDelta(event.text);
        break;
      case "thinking_start":
        this.finalizeCurrentText();
        this.currentReasoningPart = { type: "reasoning", text: "", status: "streaming" };
        this.parts.push(this.currentReasoningPart);
        break;
      case "thinking_delta":
        if (this.currentReasoningPart) {
          this.currentReasoningPart.text += event.text;
        }
        break;
      case "thinking_end":
        if (this.currentReasoningPart) {
          this.currentReasoningPart.status = "complete";
          this.currentReasoningPart = null;
        }
        break;
      case "tool_call_start": {
        this.finalizeCurrentText();
        const toolPart = {
          type: "tool_call",
          toolCallId: event.toolCallId,
          name: event.toolName,
          args: event.args,
          status: "running"
        };
        this.toolCallParts.set(event.toolCallId, toolPart);
        this.parts.push(toolPart);
        break;
      }
      case "tool_call_end": {
        const existing = this.toolCallParts.get(event.toolCallId);
        if (existing) {
          existing.result = event.result;
          existing.status = "complete";
        }
        break;
      }
      case "error":
        this.status = "error";
        break;
    }
  }
  handleTextDelta(text) {
    if (!this.currentTextPart) {
      this.currentTextPart = { type: "text", text: "", status: "streaming" };
      this.parts.push(this.currentTextPart);
    }
    this.currentTextPart.text += text;
  }
  finalizeCurrentText() {
    if (this.currentTextPart) {
      this.currentTextPart.status = "complete";
      this.currentTextPart = null;
    }
  }
  /**
   * Get a snapshot of the current accumulated message (for streaming UI)
   * @returns ChatMessage with current parts and "streaming" status
   */
  snapshot() {
    const now = (/* @__PURE__ */ new Date()).toISOString();
    return {
      id: this.messageId,
      role: "assistant",
      parts: this.parts.map((p) => ({ ...p })),
      status: this.status === "pending" ? "pending" : "streaming",
      createdAt: now,
      updatedAt: now
    };
  }
  /**
   * Finalize the accumulator and return the complete ChatMessage
   * @returns Completed ChatMessage with all parts finalized
   * @throws Error if accumulator is already finalized
   */
  finalize() {
    if (this._finalized) throw new Error("Accumulator already finalized");
    this._finalized = true;
    this.finalizeCurrentText();
    if (this.currentReasoningPart) {
      this.currentReasoningPart.status = "complete";
      this.currentReasoningPart = null;
    }
    for (const [, toolPart] of this.toolCallParts) {
      if (toolPart.status === "running" || toolPart.status === "pending") {
        toolPart.status = "error";
      }
    }
    if (this.status !== "error" && this.status !== "cancelled") {
      this.status = "complete";
    }
    const now = (/* @__PURE__ */ new Date()).toISOString();
    return {
      id: this.messageId,
      role: "assistant",
      parts: this.parts,
      status: this.status,
      createdAt: now,
      updatedAt: now
    };
  }
  /** Check if the accumulator has been finalized */
  get finalized() {
    return this._finalized;
  }
};

// src/chat/watchdog.ts
async function* withStreamWatchdog(source, config) {
  const { timeoutMs, signal } = config;
  const iterator = source[Symbol.asyncIterator]();
  let aborted = false;
  if (signal?.aborted) {
    iterator.return?.();
    return;
  }
  const onAbort = () => {
    aborted = true;
    iterator.return?.();
  };
  signal?.addEventListener("abort", onAbort, { once: true });
  try {
    while (true) {
      if (aborted) break;
      const timeout = new CancellableTimeout(timeoutMs);
      try {
        const result = await Promise.race([
          iterator.next(),
          timeout.promise
        ]);
        timeout.cancel();
        if (result.done) break;
        yield result.value;
      } catch (err) {
        timeout.cancel();
        throw err;
      }
    }
  } finally {
    signal?.removeEventListener("abort", onAbort);
    iterator.return?.();
  }
}
var CancellableTimeout = class {
  promise;
  _timer;
  _cancelled = false;
  constructor(ms) {
    this.promise = new Promise((_, reject) => {
      this._timer = setTimeout(() => {
        if (!this._cancelled) {
          reject(
            new ChatError(
              `Stream timed out after ${ms}ms of inactivity`,
              { code: "TIMEOUT" /* TIMEOUT */ }
            )
          );
        }
      }, ms);
    });
    this.promise.catch(() => {
    });
  }
  cancel() {
    this._cancelled = true;
    if (this._timer !== void 0) {
      clearTimeout(this._timer);
      this._timer = void 0;
    }
  }
};

// src/chat/listener-set.ts
var ListenerSet = class {
  _listeners = /* @__PURE__ */ new Set();
  /** Add a listener. Returns an unsubscribe function. */
  add(callback) {
    this._listeners.add(callback);
    return () => {
      this._listeners.delete(callback);
    };
  }
  /** Notify all listeners with the given arguments. Errors are isolated per listener. */
  notify(...args) {
    for (const cb of this._listeners) {
      try {
        cb(...args);
      } catch {
      }
    }
  }
  /** Remove all listeners. */
  clear() {
    this._listeners.clear();
  }
  /** Current number of listeners. */
  get size() {
    return this._listeners.size;
  }
};

// src/chat/runtime.ts
var ChatRuntime = class {
  _state;
  _guard;
  _backends;
  _sessionStore;
  _contextConfig;
  _middleware;
  _tools = /* @__PURE__ */ new Map();
  _retryConfig;
  _contextStats = /* @__PURE__ */ new Map();
  _sessionUsage = /* @__PURE__ */ new Map();
  _modelContextWindows = /* @__PURE__ */ new Map();
  _onContextTrimmed;
  _streamTimeoutMs;
  _sessionListeners = new ListenerSet();
  _adapterPool = /* @__PURE__ */ new Map();
  _defaultBackend;
  _abortController = null;
  constructor(options) {
    this._state = new StateMachine("idle", RUNTIME_TRANSITIONS);
    this._guard = new ChatReentrancyGuard();
    this._backends = options.backends;
    this._defaultBackend = options.defaultBackend;
    this._sessionStore = options.sessionStore;
    this._contextConfig = options.context;
    this._middleware = [...options.middleware ?? []];
    this._retryConfig = options.retryConfig;
    this._onContextTrimmed = options.onContextTrimmed;
    this._streamTimeoutMs = options.streamTimeoutMs;
    if (!options.backends[options.defaultBackend]) {
      throw new ChatError(
        `Default backend "${options.defaultBackend}" not found in backends map`,
        { code: "INVALID_INPUT" /* INVALID_INPUT */ }
      );
    }
    if (options.tools) {
      for (const tool of options.tools) {
        this._tools.set(tool.name, tool);
      }
    }
  }
  // ── Lifecycle ──────────────────────────────────────────────
  get status() {
    return this._state.current;
  }
  async dispose() {
    if (this._state.current === "disposed") return;
    this._abortController?.abort("Runtime disposed");
    this._abortController?.dispose();
    this._abortController = null;
    this._state.transition("disposed");
    for (const adapter of this._adapterPool.values()) {
      try {
        await adapter.dispose();
      } catch {
      }
    }
    this._adapterPool.clear();
  }
  // ── Sessions ───────────────────────────────────────────────
  async createSession(options) {
    this.assertNotDisposed();
    const config = {
      model: options.config?.model ?? "",
      backend: options.config?.backend ?? this._defaultBackend,
      ...options.config
    };
    const session = await this._sessionStore.createSession({ ...options, config });
    this._notifySessionChange();
    return session;
  }
  async getSession(id) {
    this.assertNotDisposed();
    const cid = toChatId(id);
    return this._sessionStore.getSession(cid);
  }
  async listSessions(options) {
    this.assertNotDisposed();
    return this._sessionStore.listSessions(options);
  }
  async deleteSession(id) {
    this.assertNotDisposed();
    const cid = toChatId(id);
    const session = await this._sessionStore.getSession(cid);
    if (!session) return;
    await this._sessionStore.deleteSession(cid);
    this._contextStats.delete(cid);
    this._sessionUsage.delete(cid);
    this._notifySessionChange();
  }
  // ── Messaging ──────────────────────────────────────────────
  async *send(sessionId, message, options) {
    this.validateSendInput(message, options);
    this._guard.acquire();
    const cid = toChatId(sessionId);
    this._abortController = new ChatAbortController(options?.signal);
    try {
      if (this._state.current === "error") {
        this._state.transition("idle");
      }
      this._state.transition("streaming");
      await this.loadSession(cid);
      const mwCtx = {
        sessionId: cid,
        signal: this._abortController.signal
      };
      const userMessage = await this.applyBeforeSendMiddleware(
        this.createUserMessage(message),
        mwCtx
      );
      if (userMessage === null) {
        this._state.transition("idle");
        return;
      }
      const updatedSession = await this.persistAndReload(cid, userMessage);
      const sessionForAdapter = await this.trimSessionContext(cid, updatedSession, options.model);
      const stream = await this.prepareEventStream(
        cid,
        sessionForAdapter,
        updatedSession,
        message,
        options
      );
      const accumulator = new MessageAccumulator();
      const eventSource = this._streamTimeoutMs ? withStreamWatchdog(stream, { timeoutMs: this._streamTimeoutMs, signal: this._abortController.signal }) : stream;
      for await (const event of eventSource) {
        if (this._abortController.isAborted) break;
        this.feedAccumulator(accumulator, event);
        if (event.type === "usage") {
          this._sessionUsage.set(cid, {
            promptTokens: event.promptTokens,
            completionTokens: event.completionTokens
          });
          this.updateContextStatsWithUsage(cid, event.promptTokens, event.completionTokens, options);
        }
        const processed = await this.applyOnEventMiddleware(event, mwCtx);
        if (processed) yield processed;
      }
      if (this._state.current === "disposed") return;
      await this.finalizeAssistantMessage(cid, accumulator, mwCtx);
      this._state.transition("idle");
    } catch (error) {
      const result = await this.handleSendError(error, cid);
      if (result !== null) throw result;
    } finally {
      this._guard.release();
      this._abortController?.dispose();
      this._abortController = null;
    }
  }
  // ── Send Pipeline Stages ──────────────────────────────────────
  /** Stage 1: Validate send inputs (message content + required fields). */
  validateSendInput(message, options) {
    this.assertNotDisposed();
    if (!message || message.trim().length === 0) {
      throw new ChatError("Message cannot be empty", { code: "INVALID_INPUT" /* INVALID_INPUT */ });
    }
    if (!options.model) {
      throw new ChatError(
        "options.model is required \u2014 caller must specify which model to use",
        { code: "INVALID_INPUT" /* INVALID_INPUT */ }
      );
    }
    if (!options.backend) {
      throw new ChatError(
        "options.backend is required \u2014 caller must specify which backend to use",
        { code: "INVALID_INPUT" /* INVALID_INPUT */ }
      );
    }
    if (!options.credentials) {
      throw new ChatError(
        "options.credentials is required \u2014 caller must provide authentication credentials",
        { code: "INVALID_INPUT" /* INVALID_INPUT */ }
      );
    }
  }
  /** Stage 2: Load session from store. */
  async loadSession(cid) {
    const session = await this._sessionStore.getSession(cid);
    if (!session) {
      throw new ChatError(
        `Session "${cid}" not found`,
        { code: "SESSION_NOT_FOUND" /* SESSION_NOT_FOUND */ }
      );
    }
    return session;
  }
  /** Stage 3: Apply onBeforeSend middleware pipeline. Returns null if middleware rejected the send. */
  async applyBeforeSendMiddleware(userMessage, ctx) {
    let msg = userMessage;
    for (const mw of this._middleware) {
      if (mw.onBeforeSend && msg) {
        msg = await mw.onBeforeSend(msg, ctx);
        if (msg === null) return null;
      }
    }
    return msg;
  }
  /** Stage 4: Persist user message and reload session with full history. */
  async persistAndReload(cid, userMessage) {
    await this._sessionStore.appendMessage(cid, userMessage);
    return await this._sessionStore.getSession(cid);
  }
  /** Stage 5: Auto-trim context window if configured. Returns session snapshot for adapter. */
  async trimSessionContext(cid, session, model) {
    if (!this._contextConfig) return session;
    const ctxManager = new ContextWindowManager(this._contextConfig);
    const lastUsage = this._sessionUsage.get(cid);
    const modelContextWindow = model ? this._modelContextWindows.get(model) : void 0;
    if (lastUsage && modelContextWindow) {
      const result2 = ctxManager.fitMessagesWithUsage(
        session.messages,
        lastUsage.promptTokens,
        modelContextWindow
      );
      this._contextStats.set(cid, {
        totalTokens: result2.totalTokens,
        removedCount: result2.removedCount,
        wasTruncated: result2.wasTruncated,
        availableBudget: Math.max(0, modelContextWindow - result2.totalTokens),
        realPromptTokens: lastUsage.promptTokens,
        realCompletionTokens: lastUsage.completionTokens,
        modelContextWindow
      });
      if (result2.wasTruncated && this._onContextTrimmed) {
        const keptIds = new Set(result2.messages.map((m) => m.id));
        const removed = session.messages.filter((m) => !keptIds.has(m.id));
        if (removed.length > 0) {
          try {
            this._onContextTrimmed(cid, removed);
          } catch {
          }
        }
      }
      return { ...session, messages: result2.messages };
    }
    const result = await ctxManager.fitMessagesAsync(session.messages);
    this._contextStats.set(cid, {
      totalTokens: result.totalTokens,
      removedCount: result.removedCount,
      wasTruncated: result.wasTruncated,
      availableBudget: ctxManager.availableBudget,
      modelContextWindow
    });
    if (result.wasTruncated && this._onContextTrimmed) {
      const keptIds = new Set(result.messages.map((m) => m.id));
      const removed = session.messages.filter((m) => !keptIds.has(m.id));
      if (removed.length > 0) {
        try {
          this._onContextTrimmed(cid, removed);
        } catch {
        }
      }
    }
    return { ...session, messages: result.messages };
  }
  /** Update context stats with real usage data from a usage event. */
  updateContextStatsWithUsage(cid, promptTokens, completionTokens, options) {
    const modelContextWindow = options.model ? this._modelContextWindows.get(options.model) : void 0;
    const existing = this._contextStats.get(cid);
    this._contextStats.set(cid, {
      totalTokens: promptTokens,
      removedCount: existing?.removedCount ?? 0,
      wasTruncated: existing?.wasTruncated ?? false,
      availableBudget: modelContextWindow ? Math.max(0, modelContextWindow - promptTokens) : existing?.availableBudget ?? 0,
      realPromptTokens: promptTokens,
      realCompletionTokens: completionTokens,
      modelContextWindow
    });
  }
  /** Stage 6: Prepare event stream — adapter with retry, tool injection. */
  async prepareEventStream(cid, sessionForAdapter, fullSession, message, options) {
    const adapter = await this.getOrCreateAdapterWithRetry(options.backend, options.credentials);
    const runtimeTools = this._tools.size > 0 ? this.injectToolContext([...this._tools.values()], {
      sessionId: cid,
      custom: fullSession.metadata?.custom
    }) : void 0;
    const streamOptions = {
      signal: this._abortController.signal,
      model: options.model,
      systemPrompt: options.systemPrompt,
      tools: runtimeTools
    };
    return this.createStreamWithRetry(
      adapter,
      sessionForAdapter,
      message,
      streamOptions,
      options.backend,
      options.credentials
    );
  }
  /** Stage 7: Apply onEvent middleware pipeline (sequential transform/suppress). */
  async applyOnEventMiddleware(event, ctx) {
    let processed = event;
    for (const mw of this._middleware) {
      if (mw.onEvent && processed) {
        processed = await mw.onEvent(processed, ctx);
      }
    }
    return processed;
  }
  /** Stage 8: Finalize accumulator, apply afterReceive middleware, persist assistant message. */
  async finalizeAssistantMessage(cid, accumulator, ctx) {
    let assistantMessage = accumulator.finalize();
    for (const mw of this._middleware) {
      if (mw.onAfterReceive) {
        assistantMessage = await mw.onAfterReceive(assistantMessage, ctx);
      }
    }
    await this._sessionStore.appendMessage(cid, assistantMessage);
    this._notifySessionChange();
  }
  /** Stage 9: Error handling — apply onError middleware, transition state. Returns null if suppressed. */
  async handleSendError(error, cid) {
    let processedError = error instanceof Error ? error : new Error(String(error));
    const ctx = {
      sessionId: cid,
      signal: this._abortController?.signal ?? new AbortController().signal
    };
    for (const mw of this._middleware) {
      if (mw.onError) {
        const result = await mw.onError(processedError, ctx);
        if (result === null) {
          if (this._state.canTransition("idle")) {
            this._state.transition("idle");
          }
          return null;
        }
        processedError = result;
      }
    }
    if (this._state.canTransition("error")) {
      this._state.transition("error");
    }
    return processedError;
  }
  abort() {
    this._abortController?.abort("User abort");
  }
  // ── Backend / Model ────────────────────────────────────────
  async listModels(options) {
    this.assertNotDisposed();
    let models = [];
    const firstAdapter = [...this._adapterPool.values()][0];
    if (firstAdapter) {
      try {
        models = await firstAdapter.listModels();
      } catch {
        return [];
      }
    } else if (options?.backend && options?.credentials) {
      try {
        const adapter = await this.getOrCreateAdapter(options.backend, options.credentials);
        models = await adapter.listModels();
      } catch {
        return [];
      }
    }
    for (const model of models) {
      if (model.contextWindow != null) {
        this._modelContextWindows.set(model.id, model.contextWindow);
      }
    }
    return models;
  }
  async listBackends() {
    this.assertNotDisposed();
    return Object.keys(this._backends).map((name) => ({ name }));
  }
  // ── Tools ──────────────────────────────────────────────────
  get registeredTools() {
    return this._tools;
  }
  registerTool(tool) {
    this.assertNotDisposed();
    this._tools.set(tool.name, tool);
  }
  removeTool(name) {
    this.assertNotDisposed();
    this._tools.delete(name);
  }
  // ── Middleware ──────────────────────────────────────────────
  use(middleware) {
    this.assertNotDisposed();
    this._middleware.push(middleware);
  }
  removeMiddleware(middleware) {
    this.assertNotDisposed();
    const idx = this._middleware.indexOf(middleware);
    if (idx >= 0) this._middleware.splice(idx, 1);
  }
  // ── Context Stats ─────────────────────────────────────────
  async getContextStats(sessionId) {
    const cid = toChatId(sessionId);
    return this._contextStats.get(cid) ?? null;
  }
  // ── Session Subscription ──────────────────────────────────
  onSessionChange(callback) {
    return this._sessionListeners.add(callback);
  }
  _notifySessionChange() {
    this._sessionListeners.notify();
  }
  // ── Private Helpers ────────────────────────────────────────
  async getOrCreateAdapter(backend, credentials) {
    const key = this.getPoolKey(backend, credentials);
    const existing = this._adapterPool.get(key);
    if (existing) return existing;
    for (const [oldKey, oldAdapter] of this._adapterPool) {
      if (oldKey.startsWith(backend + ":")) {
        try {
          await oldAdapter.dispose();
        } catch {
        }
        this._adapterPool.delete(oldKey);
      }
    }
    const factory = this._backends[backend];
    if (!factory) {
      throw new ChatError(
        `Backend "${backend}" not found`,
        { code: "INVALID_INPUT" /* INVALID_INPUT */ }
      );
    }
    const adapter = await factory(credentials);
    this._adapterPool.set(key, adapter);
    return adapter;
  }
  getPoolKey(backend, credentials) {
    const token = credentials.accessToken;
    const hash = token.length > 16 ? token.slice(0, 8) + token.slice(-8) : token;
    return `${backend}:${hash}`;
  }
  /** Wrap each tool's execute to inject ToolContext as 2nd argument */
  injectToolContext(tools, context) {
    return tools.map((tool) => ({
      ...tool,
      execute: (params) => tool.execute(params, context)
    }));
  }
  /** Map ChatEvent to AgentEvent for MessageAccumulator */
  feedAccumulator(acc, event) {
    const agentEvent = chatEventToAgentEvent(event);
    if (agentEvent) acc.apply(agentEvent);
  }
  createUserMessage(text) {
    return {
      id: createChatId(),
      role: "user",
      parts: [{ type: "text", text, status: "complete" }],
      createdAt: (/* @__PURE__ */ new Date()).toISOString(),
      status: "complete"
    };
  }
  assertNotDisposed() {
    if (this._state.current === "disposed") {
      throw new ChatError(
        "Runtime is disposed",
        { code: "DISPOSED" /* DISPOSED */ }
      );
    }
  }
  /** Get or create adapter with retry on connection errors */
  async getOrCreateAdapterWithRetry(backend, credentials) {
    const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
    const delayMs = this._retryConfig?.delayMs ?? 0;
    let lastError;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        return await this.getOrCreateAdapter(backend, credentials);
      } catch (err) {
        lastError = err instanceof Error ? err : new Error(String(err));
        if (attempt < maxAttempts) {
          const key = this.getPoolKey(backend, credentials);
          const old = this._adapterPool.get(key);
          if (old) {
            try {
              await old.dispose();
            } catch {
            }
          }
          this._adapterPool.delete(key);
          await delay(delayMs);
        }
      }
    }
    throw lastError;
  }
  /**
   * Create stream with retry for pre-stream connection errors.
   * Tries to get the first event from the stream; if that fails,
   * retries with a fresh adapter. Once first event is received,
   * the stream is committed (no more retries).
   */
  async createStreamWithRetry(adapter, session, message, options, backend, credentials) {
    const maxAttempts = this._retryConfig?.maxAttempts ?? 1;
    const delayMs = this._retryConfig?.delayMs ?? 0;
    let lastError;
    let currentAdapter = adapter;
    for (let attempt = 1; attempt <= maxAttempts; attempt++) {
      try {
        const stream = currentAdapter.streamMessage(session, message, options);
        const iterator = stream[Symbol.asyncIterator]();
        const first = await iterator.next();
        return (async function* () {
          if (!first.done) yield first.value;
          while (true) {
            const next = await iterator.next();
            if (next.done) break;
            yield next.value;
          }
        })();
      } catch (err) {
        lastError = err instanceof Error ? err : new Error(String(err));
        if (attempt < maxAttempts) {
          try {
            await currentAdapter.dispose();
          } catch {
          }
          const key = this.getPoolKey(backend, credentials);
          this._adapterPool.delete(key);
          await delay(delayMs);
          currentAdapter = await this.getOrCreateAdapter(backend, credentials);
        }
      }
    }
    throw lastError;
  }
};
function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
function createChatRuntime(options) {
  return new ChatRuntime(options);
}

exports.createChatRuntime = createChatRuntime;
//# sourceMappingURL=runtime.cjs.map
//# sourceMappingURL=runtime.cjs.map