UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

1,112 lines (1,111 loc) 42.5 kB
import { r as defaultRuntime } from "./runtime-CF2WjnNZ.js"; import { g as registerConfigWriteListener, n as getRuntimeConfig } from "./io.runtime-B9iJRs3w.js"; import { i as waitForAbortSignal } from "./abort-signal-D2k14JsD.js"; import { n as normalizeAgentId } from "./agent-id-CeT3w4ap.js"; import { i as listAgentEntries, m as resolveAgentWorkspaceDir, u as resolveAgentDir, y as resolveDefaultAgentId } from "./agent-scope-config-DcbEhP0R.js"; import { i as agentSessionKeysMatchByRequestKey } from "./session-key-BnWWjqNc.js"; import { n as buildAllowedModelSet, r as buildConfiguredModelCatalog } from "./model-selection-shared-BlLyx1r2.js"; import { r as DEFAULT_PROVIDER } from "./defaults-CdX9UGcX.js"; import { _ as resolveSessionAgentId } from "./agent-scope-DbtJyKUL.js"; import "./config-Cs0XXL3x.js"; import { i as logWarn, r as logInfo } from "./logger-DwECwNVZ.js"; import { d as onAgentEvent } from "./agent-events-CoxiItUi.js"; import { f as withPluginRuntimeRegistryScope } from "./gateway-request-scope-BCMYlsDI.js"; import { t as INTERNAL_MESSAGE_CHANNEL } from "./message-channel-constants-2zSoJXQC.js"; import "./message-channel-BQrhwUEA.js"; import { t as CHAT_HISTORY_MAX_ENTRIES } from "./chat-history-constants-C-H8nkgi.js"; import { H as EmbeddedPluginApprovalBroker, K as setEmbeddedMode, U as clearEmbeddedPluginApprovalBroker, W as setEmbeddedPluginApprovalBroker } from "./agent-tools.before-tool-call-Bb7DJuFB.js"; import { Lt as applySessionPatchProjection } from "./session-accessor-YsytfDtG.js"; import { u as getMaxChatHistoryMessagesBytes } from "./server-constants-BrVEC7RW.js"; import { t as loadAgentRuntimePluginRegistryHandle } from "./runtime-plugins-BDPJ7y4t.js"; import { d as refreshPreparedModelRuntimeSnapshots } from "./prepared-model-runtime-DgNj94yb.js"; import "./model-selection-di2kjKCB.js"; import { n as resolveThinkingDefault } from "./model-thinking-default-1g_x1V4J.js"; import { c as isDefinitiveRunLifecycle, i as buildAgentRunTerminalOutcomeFromLifecycleEvent, o as classifyAgentRunTerminalOutcome, t as AGENT_RUN_TERMINAL_RETRY_GRACE_MS } from "./agent-run-terminal-outcome-CigeY75d.js"; import { i as resolveTextCommand } from "./commands-registry-normalize-Bugd8r0v.js"; import { t as QuestionAnswerUnconfirmedError } from "./gateway-question-dispatch-BkYi3BfY.js"; import { v as queueEmbeddedAgentMessageWithOutcomeAsync } from "./runs-Cb42qain.js"; import { i as resolveActiveEmbeddedRunSessionId } from "./active-run-projections-C4mdt8WG.js"; import { n as loadCombinedSessionStoreForGatewayCore } from "./combined-store-gateway-DGm4nZAJ.js"; import { i as parseGoalCommand, t as executeSessionGoalCommand } from "./commands-goal-B_2zXG79.js"; import { n as findAgentRunTerminalOutcome } from "./agent-run-terminal-error-BLySVFXs.js"; import { v as capArrayByJsonBytes } from "./session-transcript-readers-CYDRQsH5.js"; import { f as resolveEffectiveChatHistoryMaxChars } from "./chat-display-projection.helpers-DYkWC7LH.js"; import { n as resolveSessionModelRef } from "./session-model-ref-CPZiclLt.js"; import { t as getSessionDefaults } from "./session-utils-model-BYAclq2V.js"; import { i as buildGatewaySessionInfo, n as listSessionsFromStoreAsync } from "./session-utils-list-B0k8KJn5.js"; import { t as ensureContextWindowCacheLoaded } from "./context-B0EuZFRZ.js"; import { f as resolveGatewaySessionStoreTargetWithStore, i as loadGatewaySessionEntryReadOnly, n as listAgentsForGateway, o as resolveCanonicalGatewaySessionStoreKey, r as loadGatewaySessionEntry } from "./session-utils-store-CInT2loy.js"; import { C as buildCollectPrompt, O as previewQueueSummaryPrompt, j as waitForQueueDebounce, t as resolveQueueSettingsCore, x as applyQueueDropPolicy } from "./settings-jmDsm5RI.js"; import "./session-utils-Cai0_C6U.js"; import { r as readToolValidationErrorSummary } from "./tool-error-summary-Bw_A4yhp.js"; import "./commands-registry-zRoXYk7P.js"; import { a as isChatStopCommandText } from "./chat-abort-qLn3eFOg.js"; import { c as projectLiveAssistantBufferedText, d as shouldSuppressAssistantEventForLiveChat, l as resolveAssistantLiveChatInput, s as normalizeLiveAssistantBufferedText, u as resolveMergedAssistantText } from "./server-chat-state-CwKZZaYd.js"; import { t as createDefaultDeps } from "./deps-CSNtqiEp.js"; import { t as loadGatewayModelCatalog } from "./server-model-catalog-CMrsE9Jh.js"; import { r as agentCommandFromIngress } from "./agent-command-DK8Hh-Mq.js"; import { s as isAgentLifecycleYieldedWaiting } from "./session-lifecycle-state-DCd4j8Bd.js"; import { a as readChatHistoryPage, c as CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES, i as enrichChatHistoryCompactionMarkers, l as replaceOversizedChatHistoryMessages } from "./chat-IsrqkYID.js"; import { s as performGatewaySessionReset } from "./session-reset-service-DsSXKjai.js"; import { n as createGatewaySession } from "./session-create-service-CM4MxLMO.js"; import { n as projectSessionsPatchEntry } from "./sessions-patch-CCbzG5TT.js"; import { t as resolveLocalRunShutdownGraceMs } from "./local-run-shutdown-CGxQk6FN.js"; import { l as formatTuiErrorMessage } from "./tui-formatters-ojaHI7ji.js"; import { randomUUID } from "node:crypto"; //#region src/tui/embedded-prepared-runtime.ts var EmbeddedPreparedModelRuntimeHost = class { constructor() { this.ready = Promise.resolve(); } publish(config) { this.ready = refreshPreparedModelRuntimeSnapshots(config, { catalogMode: "static" }); } async waitUntilReady() { for (;;) { const ready = this.ready; await ready; if (ready === this.ready) return; } } }; //#endregion //#region src/tui/embedded-backend.ts const TUI_STATE_BY_TERMINAL_CLASSIFICATION = { success: void 0, timeout: "error", cancellation: "aborted", failure: "error" }; const silentRuntime = { log: (..._args) => void 0, error: (..._args) => void 0, exit: (code) => { throw new Error(`embedded tui runtime exit ${String(code)}`); } }; const embeddedSessionStartupMigrationLog = { info: (message) => logInfo(message, silentRuntime), warn: (message) => logWarn(message, silentRuntime) }; function hasProviderWildcardModelAllowlist(cfg) { return [cfg.agents?.defaults?.models, ...listAgentEntries(cfg).map((agent) => agent.models)].some((models) => Object.keys(models ?? {}).some((key) => key.trim().endsWith("/*"))); } function resolveConfiguredReplaceModeCatalog(cfg) { if (cfg.models?.mode !== "replace") return; if (hasProviderWildcardModelAllowlist(cfg)) return; return buildConfiguredModelCatalog({ cfg }); } function shouldLoadFullGatewayCatalogForReplaceMode(cfg) { return cfg.models?.mode === "replace" && hasProviderWildcardModelAllowlist(cfg); } function ensureEmbeddedHistoryRuntimePluginsLoaded(params) { try { const workspaceDir = resolveAgentWorkspaceDir(params.cfg, params.sessionAgentId); const registry = loadAgentRuntimePluginRegistryHandle({ config: params.cfg, workspaceDir }); return { status: "warmed", ...registry ? { registry } : {} }; } catch (err) { return { status: "failed", error: formatTuiErrorMessage(err) }; } } async function loadEmbeddedTuiModelCatalog(cfg) { const configuredCatalog = resolveConfiguredReplaceModeCatalog(cfg); if (configuredCatalog !== void 0) return configuredCatalog; return await loadGatewayModelCatalog(shouldLoadFullGatewayCatalogForReplaceMode(cfg) ? { readOnly: false } : void 0); } function resolveBtwQuestion(message) { const question = /^\/(?:btw|side)(?::|\s)+(.*)$/i.exec(message.trim())?.[1]?.trim(); return question ? question : void 0; } function buildLocalQueuedPrompt(queue) { return [previewQueueSummaryPrompt({ state: queue, noun: "message" }), queue.mode === "collect" && queue.messages.length > 1 ? buildCollectPrompt({ title: "[Queued messages while agent was busy]", items: queue.messages, renderItem: (message, index) => `---\nQueued #${index + 1}\n${message}` }) : queue.messages[0] ?? ""].filter(Boolean).join("\n\n"); } function payloadText(parts) { if (!Array.isArray(parts)) return ""; return parts.map((part) => { if (!part || typeof part !== "object") return ""; const payload = part; return typeof payload.text === "string" ? payload.text.trim() : ""; }).filter(Boolean).join("\n\n").trim(); } function assistantChatMessage(text) { return { role: "assistant", content: [{ type: "text", text }], timestamp: Date.now() }; } function timeoutSecondsFromMs(timeoutMs) { if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs < 0) return; return String(Math.max(0, Math.ceil(timeoutMs / 1e3))); } function resolveDeltaPayload(text, previousText) { if (previousText === void 0) return { deltaText: text }; if (!text.startsWith(previousText)) return { deltaText: text, replace: true }; return { deltaText: text.slice(previousText.length) }; } function createQueuedRunReadiness() { let markReady; return { promise: new Promise((ready) => { markReady = ready; }), markReady }; } async function waitForLocalRunShutdown(promises) { if (promises.length === 0) return true; const timeoutMs = resolveLocalRunShutdownGraceMs(); if (timeoutMs <= 0) return false; let timeout; let completed = false; await Promise.race([Promise.allSettled(promises).then(() => { completed = true; }), new Promise((resolve) => { timeout = setTimeout(resolve, timeoutMs); timeout.unref?.(); })]); if (timeout) clearTimeout(timeout); return completed; } async function waitForQueuedLocalRun(previousRun, runId) { await previousRun.run.queuedRunReady; if (previousRun.run.controller.signal.aborted && previousRun.run.queuedAfter) return await waitForQueuedLocalRun(previousRun.run.queuedAfter, runId); if (!previousRun.run.finishing && !previousRun.run.lifecycleEnded) { await previousRun.promise; return; } const timeoutMs = resolveLocalRunShutdownGraceMs(); if (timeoutMs <= 0) throw new Error(`timed out waiting for previous local run to finish post-turn maintenance for ${runId}`); let timeout; try { await Promise.race([previousRun.promise, new Promise((_, reject) => { timeout = setTimeout(() => { reject(/* @__PURE__ */ new Error(`timed out waiting for previous local run to finish post-turn maintenance for ${runId}`)); }, timeoutMs); timeout.unref?.(); })]); } finally { if (timeout) clearTimeout(timeout); } } var EmbeddedTuiBackend = class { constructor() { this.connection = { url: "local embedded" }; this.deps = createDefaultDeps(); this.runs = /* @__PURE__ */ new Map(); this.runPromises = /* @__PURE__ */ new Map(); this.seq = 0; this.pendingLifecycleErrors = /* @__PURE__ */ new Map(); this.pluginApprovalBroker = new EmbeddedPluginApprovalBroker(); this.preparedModelRuntime = new EmbeddedPreparedModelRuntimeHost(); this.ready = Promise.resolve(); } withRuntimePluginRegistry(run) { return withPluginRuntimeRegistryScope(this.runtimePluginRegistry, run); } start() { if (this.unsubscribe) return; setEmbeddedMode(true); ensureContextWindowCacheLoaded(); this.previousRuntimeLog = defaultRuntime.log; this.previousRuntimeError = defaultRuntime.error; defaultRuntime.log = silentRuntime.log; defaultRuntime.error = silentRuntime.error; this.unsubscribe = onAgentEvent((evt) => this.handleAgentEvent(evt)); setEmbeddedPluginApprovalBroker(this.pluginApprovalBroker); this.unsubscribePluginApprovals = this.pluginApprovalBroker.subscribe((event) => { this.emit(event.event, event.payload); }); const config = getRuntimeConfig(); this.unsubscribeConfigWrites = registerConfigWriteListener((event) => { this.preparedModelRuntime.publish(event.runtimeConfig); }); this.preparedModelRuntime.publish(config); this.ready = (async () => { const { runSessionStartupMigration } = await import("./startup-migration-BV-eQ791.js"); await runSessionStartupMigration({ cfg: config, env: process.env, log: embeddedSessionStartupMigrationLog }); })(); queueMicrotask(() => { this.onConnected?.(); }); } async stop() { this.unsubscribeConfigWrites?.(); this.unsubscribeConfigWrites = void 0; clearEmbeddedPluginApprovalBroker(this.pluginApprovalBroker); this.unsubscribePluginApprovals?.(); this.unsubscribePluginApprovals = void 0; const maintenancePromises = []; for (const [runId, run] of this.runs) { if (run.finishing || run.lifecycleEnded) { const promise = this.runPromises.get(runId); if (promise) maintenancePromises.push(promise); continue; } run.controller.abort(); } this.pluginApprovalBroker.stop(); if (!await waitForLocalRunShutdown(maintenancePromises)) { for (const run of this.runs.values()) if (run.finishing || run.lifecycleEnded) run.controller.abort(); } this.unsubscribe?.(); this.unsubscribe = void 0; this.pendingLifecycleErrors.forEach(clearTimeout); this.pendingLifecycleErrors.clear(); for (const run of this.runs.values()) run.controller.abort(); this.runs.clear(); this.runPromises.clear(); defaultRuntime.log = this.previousRuntimeLog ?? defaultRuntime.log; defaultRuntime.error = this.previousRuntimeError ?? defaultRuntime.error; this.previousRuntimeLog = void 0; this.previousRuntimeError = void 0; setEmbeddedMode(false); } async sendChat(opts) { await this.ready; await this.preparedModelRuntime.waitUntilReady(); const runId = opts.runId ?? randomUUID(); const question = resolveBtwQuestion(opts.message); const isQueueCommand = resolveTextCommand(opts.message)?.command.key === "queue"; const agentId = resolveSessionAgentId({ sessionKey: opts.sessionKey, config: getRuntimeConfig(), agentId: opts.agentId }); const runScope = { sessionKey: opts.sessionKey, agentId }; const stopCommand = this.hasAbortableSessionRun(runScope) && isChatStopCommandText(opts.message); const queuedAfter = question || stopCommand || isQueueCommand ? void 0 : this.findQueuedSessionRunPromise(runScope); if (stopCommand) { this.abortSessionRuns(runScope); return { runId }; } let pendingQueue; if (queuedAfter) { const loadOptions = opts.agentId ? { agentId: opts.agentId } : void 0; const { cfg, canonicalKey, entry } = loadGatewaySessionEntry(opts.sessionKey, loadOptions); let queueSettings = resolveQueueSettingsCore({ cfg, channel: INTERNAL_MESSAGE_CHANNEL, sessionEntry: entry }); if (queueSettings.mode === "steer") { const activeSessionId = resolveActiveEmbeddedRunSessionId(canonicalKey); if (activeSessionId) { if ((await queueEmbeddedAgentMessageWithOutcomeAsync(activeSessionId, opts.message, { steeringMode: "all", debounceMs: queueSettings.debounceMs ?? 500, isInboundUserMessage: true }).catch((error) => { if (error instanceof QuestionAnswerUnconfirmedError) throw error; }))?.queued) return { runId: queuedAfter.runId }; } queueSettings = { ...queueSettings, mode: "followup" }; } if (queueSettings.mode === "interrupt") this.abortSessionRuns(runScope); else { const queued = this.enqueuePendingLocalMessage({ runScope, message: opts.message, settings: queueSettings, fallbackRunId: queuedAfter.runId }); if (queued.kind === "handled") return { runId: queued.runId }; pendingQueue = queued.queue; } } const controller = new AbortController(); const queuedRunReadiness = createQueuedRunReadiness(); this.runs.set(runId, { sessionKey: opts.sessionKey, agentId, controller, buffer: "", managedMediaUrls: /* @__PURE__ */ new Set(), isBtw: Boolean(question), question, finishing: false, lifecycleEnded: false, registered: false, ...pendingQueue ? { pendingQueue } : {}, ...queuedAfter ? { queuedAfter } : {}, queuedRunReady: queuedRunReadiness.promise, markQueuedRunReady: queuedRunReadiness.markReady }); const runPromise = this.runTurn({ runId, sessionKey: opts.sessionKey, agentId: opts.agentId, message: opts.message, thinking: opts.thinking, deliver: opts.deliver, timeoutMs: opts.timeoutMs, controller, queuedAfter }); this.runPromises.set(runId, runPromise); runPromise.finally(() => { this.runPromises.delete(runId); }); if (isQueueCommand) await runPromise; return { runId }; } async abortChat(opts) { if (!opts.runId) { let aborted = false; const runIds = []; for (const [runId, run] of this.runs) { if (run.isBtw) continue; if (run.sessionKey !== opts.sessionKey) continue; if (opts.sessionKey === "global") { const defaultAgentId = resolveDefaultAgentId(getRuntimeConfig()); const requestedAgentId = opts.agentId ? normalizeAgentId(opts.agentId) : defaultAgentId; if ((run.agentId ? normalizeAgentId(run.agentId) : defaultAgentId) !== requestedAgentId) continue; } if (!this.isAbortableRun(runId, run)) continue; run.controller.abort(); aborted = true; runIds.push(runId); } return { ok: true, aborted, runIds }; } const run = this.runs.get(opts.runId); if (!run || run.sessionKey !== opts.sessionKey) return { ok: true, aborted: false, runIds: [] }; if (opts.sessionKey === "global") { const defaultAgentId = resolveDefaultAgentId(getRuntimeConfig()); const requestedAgentId = opts.agentId ? normalizeAgentId(opts.agentId) : defaultAgentId; if ((run.agentId ? normalizeAgentId(run.agentId) : defaultAgentId) !== requestedAgentId) return { ok: true, aborted: false, runIds: [] }; } if (!this.isAbortableRun(opts.runId, run)) return { ok: true, aborted: false, runIds: [] }; run.controller.abort(); return { ok: true, aborted: true, runIds: [opts.runId] }; } async loadHistory(opts) { await this.ready; const loadOptions = opts.agentId ? { agentId: opts.agentId } : void 0; const { cfg, storePath, store, entry, canonicalKey } = loadGatewaySessionEntryReadOnly(opts.sessionKey, { ...loadOptions, includeStoreChildEntries: true }); const sessionId = entry?.sessionId; const sessionAgentId = resolveSessionAgentId({ sessionKey: opts.sessionKey, config: cfg, agentId: opts.agentId }); const runtimePluginsPrewarm = ensureEmbeddedHistoryRuntimePluginsLoaded({ cfg, sessionAgentId }); this.runtimePluginRegistry = runtimePluginsPrewarm.status === "warmed" ? runtimePluginsPrewarm.registry : void 0; const resolvedSessionModel = resolveSessionModelRef(cfg, entry, sessionAgentId); const max = Math.min(CHAT_HISTORY_MAX_ENTRIES, typeof opts.limit === "number" ? opts.limit : 200); const maxHistoryBytes = getMaxChatHistoryMessagesBytes(); const effectiveMaxChars = resolveEffectiveChatHistoryMaxChars(cfg); const historyPage = await readChatHistoryPage({ entry, provider: resolvedSessionModel.provider, sessionId, storePath, sessionAgentId, canonicalKey, max, maxHistoryBytes, effectiveMaxChars, offset: void 0, messageId: void 0 }); const normalized = enrichChatHistoryCompactionMarkers(historyPage.messages, entry); const replaced = replaceOversizedChatHistoryMessages({ messages: normalized, maxSingleMessageBytes: Math.min(CHAT_HISTORY_MAX_SINGLE_MESSAGE_BYTES, maxHistoryBytes) }); const messages = capArrayByJsonBytes(replaced.messages, maxHistoryBytes).items; const newestInFlightRun = [...this.runs.entries()].findLast(([, run]) => !run.isBtw && run.terminalState !== "final" && agentSessionKeysMatchByRequestKey(run.sessionKey, opts.sessionKey) && normalizeAgentId(run.agentId) === normalizeAgentId(sessionAgentId)); const inFlightRun = newestInFlightRun ? { runId: newestInFlightRun[0], text: projectLiveAssistantBufferedText(normalizeLiveAssistantBufferedText(newestInFlightRun[1].buffer, { managedMediaUrls: [...newestInFlightRun[1].managedMediaUrls] }).trim(), { suppressLeadFragments: true }).text.trim() } : void 0; let thinkingLevel = entry?.thinkingLevel; if (!thinkingLevel) { const catalog = await this.withRuntimePluginRegistry(() => loadEmbeddedTuiModelCatalog(cfg)); thinkingLevel = resolveThinkingDefault({ cfg, provider: resolvedSessionModel.provider, model: resolvedSessionModel.model, catalog }); } const defaults = getSessionDefaults(cfg, void 0, { allowPluginNormalization: false }); const sessionInfo = buildGatewaySessionInfo({ cfg, storePath, store, key: canonicalKey, entry, agentId: opts.agentId }); sessionInfo.thinkingLevel = thinkingLevel; sessionInfo.verboseLevel = entry?.verboseLevel ?? cfg.agents?.defaults?.verboseDefault; return { sessionKey: opts.sessionKey, sessionId, messages, defaults, sessionInfo, thinkingLevel, fastMode: entry?.fastMode, verboseLevel: sessionInfo.verboseLevel, runtimePluginsPrewarm: runtimePluginsPrewarm.status === "warmed" ? { status: "warmed" } : runtimePluginsPrewarm, ...inFlightRun ? { inFlightRun } : {} }; } async listSessions(opts) { await this.ready; const cfg = getRuntimeConfig(); const { storePath, store } = loadCombinedSessionStoreForGatewayCore(cfg, { agentId: opts?.agentId, projection: "list" }); return await listSessionsFromStoreAsync({ cfg, storePath, store, opts: opts ?? {} }); } async listAgents() { return listAgentsForGateway(getRuntimeConfig()); } async patchSession(opts) { await this.ready; const cfg = getRuntimeConfig(); const target = resolveGatewaySessionStoreTargetWithStore({ cfg, key: opts.key, agentId: opts.agentId, exactRead: true }); const applied = await applySessionPatchProjection({ ...opts.label === void 0 ? { sessionKeys: target.storeKeys } : {}, storePath: target.storePath, resolveTarget: ({ store }) => { const { target: migratedTarget, primaryKey } = resolveCanonicalGatewaySessionStoreKey({ cfg, key: opts.key, store, agentId: opts.agentId }); return { primaryKey, candidateKeys: migratedTarget.storeKeys }; }, project: async ({ primaryKey, existingEntry, isLabelInUse }) => await projectSessionsPatchEntry({ cfg, existingEntry, isLabelInUse, storeKey: primaryKey, agentId: opts.agentId, patch: opts, loadGatewayModelCatalog: () => this.withRuntimePluginRegistry(() => loadEmbeddedTuiModelCatalog(cfg)) }) }); if (!applied.ok) throw new Error(applied.error.message); const agentId = resolveSessionAgentId({ sessionKey: target.canonicalKey ?? opts.key, config: cfg, agentId: opts.agentId }); const resolved = resolveSessionModelRef(cfg, applied.entry, agentId); return { ok: true, path: target.storePath, key: target.canonicalKey ?? opts.key, entry: { ...applied.entry }, resolved: { modelProvider: resolved.provider, model: resolved.model } }; } async resetSession(key, reason, opts) { await this.ready; if (loadGatewaySessionEntryReadOnly(key, opts).entry?.incognito === true) throw new Error("Incognito sessions cannot reset in place."); const result = await performGatewaySessionReset({ key, operatorRoleActor: { kind: "system" }, ...opts?.agentId ? { agentId: opts.agentId } : {}, reason: reason === "new" ? "new" : "reset", commandSource: "tui:embedded", armSessionDiffBaselineCapture: true }); if (!result.ok) throw new Error(result.error.message); if ("incognitoDeleted" in result) return { ok: true, key: result.key, deleted: true }; return { ok: true, key: result.key, entry: result.entry, resolved: result.resolved }; } async createSession(opts) { await this.ready; const cfg = getRuntimeConfig(); const result = await createGatewaySession({ cfg, operatorRoleActor: { kind: "system" }, ...opts, creation: { via: "operator", actor: { type: "human", source: "unknown" } }, armSessionDiffBaselineCapture: true, emitCommandHooks: Boolean(opts.parentSessionKey), commandSource: "tui:embedded", loadGatewayModelCatalog: () => this.withRuntimePluginRegistry(() => loadEmbeddedTuiModelCatalog(cfg)) }); if (!result.ok) throw new Error(result.error.message); return { ok: true, key: result.key, entry: result.entry, resolved: result.resolved }; } async runBtwTurn(params) { const loadOptions = params.agentId ? { agentId: params.agentId } : void 0; const { cfg, canonicalKey, storePath, store, entry } = loadGatewaySessionEntry(params.sessionKey, loadOptions); if (!entry?.sessionId) throw new Error("/btw requires an active session with existing context."); const sessionAgentId = resolveSessionAgentId({ sessionKey: canonicalKey, config: cfg, agentId: params.agentId }); const resolvedModel = resolveSessionModelRef(cfg, entry, sessionAgentId); const timeoutSeconds = timeoutSecondsFromMs(params.timeoutMs); const { runBtwSideQuestion } = await import("./btw-B0DuTy-I.js"); const reply = await runBtwSideQuestion({ cfg, agentId: sessionAgentId, agentDir: resolveAgentDir(cfg, sessionAgentId), provider: resolvedModel.provider, model: resolvedModel.model, question: params.question, sessionEntry: entry, sessionStore: store, sessionKey: canonicalKey, storePath, resolvedThinkLevel: "off", resolvedReasoningLevel: "off", opts: { runId: params.runId, abortSignal: params.controller.signal, ...timeoutSeconds !== void 0 ? { timeoutOverrideSeconds: Number(timeoutSeconds) } : {} }, isNewSession: false, messageChannel: INTERNAL_MESSAGE_CHANNEL, messageProvider: INTERNAL_MESSAGE_CHANNEL, currentChannelId: INTERNAL_MESSAGE_CHANNEL }); const text = reply?.text?.trim() ?? ""; if (!text) throw new Error("/btw produced no answer."); return { sessionKey: canonicalKey, text, isError: reply?.isError === true }; } async getGatewayStatus() { return `local embedded mode${this.runs.size > 0 ? ` (${String(this.runs.size)} active run${this.runs.size === 1 ? "" : "s"})` : ""}`; } async listPluginApprovals() { return this.pluginApprovalBroker.listPending(); } async resolvePluginApproval(id, decision) { return { ok: this.pluginApprovalBroker.resolve(id, decision) }; } async listModels(opts) { const cfg = getRuntimeConfig(); const catalog = await this.withRuntimePluginRegistry(() => loadEmbeddedTuiModelCatalog(cfg)); const { allowedCatalog } = buildAllowedModelSet({ cfg, catalog, defaultProvider: DEFAULT_PROVIDER, agentId: opts?.agentId }); return allowedCatalog.map((entry) => ({ id: entry.id, name: entry.name ?? entry.id, provider: entry.provider, contextWindow: entry.contextWindow, reasoning: entry.reasoning })); } async runGoalCommand(opts) { await this.ready; const loadOptions = opts.agentId ? { agentId: opts.agentId } : void 0; const { canonicalKey, storePath, entry } = loadGatewaySessionEntry(opts.sessionKey, loadOptions); const sessionKey = canonicalKey ?? opts.sessionKey; const parsed = parseGoalCommand(opts.command.trim()); if (!parsed) throw new Error("invalid goal command"); const result = await executeSessionGoalCommand({ parsed, sessionKey, storePath, fallbackEntry: entry ?? { sessionId: randomUUID(), updatedAt: Date.now() }, agentId: opts.agentId }); return result.continuationPrompt ? { text: result.text, continuationPrompt: result.continuationPrompt } : { text: result.text }; } async runUsageCostCommand(opts) { await this.ready; const { cfg, canonicalKey, storePath, entry } = loadGatewaySessionEntry(opts.sessionKey, opts.agentId ? { agentId: opts.agentId } : void 0); const { formatSessionUsageCostSummary } = await import("./commands-session-cost.runtime.js"); return { text: await formatSessionUsageCostSummary({ cfg, sessionKey: canonicalKey ?? opts.sessionKey, agentId: opts.agentId, sessionEntry: entry, storePath }) }; } enqueuePendingLocalMessage(params) { const pendingMessages = this.listPendingLocalMessages(params.runScope); const overflowQueue = { items: [...pendingMessages], cap: params.settings.cap ?? 20, dropPolicy: params.settings.dropPolicy ?? "summarize", droppedCount: 0, summaryLines: [] }; if (!applyQueueDropPolicy({ queue: overflowQueue, summarize: (item) => item.message })) return { kind: "handled", runId: params.fallbackRunId }; const retained = new Set(overflowQueue.items); const droppedByRun = /* @__PURE__ */ new Map(); for (const dropped of pendingMessages) { if (retained.has(dropped)) continue; const indices = droppedByRun.get(dropped.run) ?? []; indices.push(dropped.messageIndex); droppedByRun.set(dropped.run, indices); } const inheritedSummaryLines = []; for (const [run, indices] of droppedByRun) { for (const index of indices.toSorted((a, b) => b - a)) run.pendingQueue?.messages.splice(index, 1); if (run.pendingQueue?.messages.length === 0) { inheritedSummaryLines.push(...run.pendingQueue.summaryLines); overflowQueue.droppedCount += run.pendingQueue.droppedCount; run.controller.abort(); } } overflowQueue.summaryLines.unshift(...inheritedSummaryLines); if (overflowQueue.summaryLines.length > overflowQueue.cap) overflowQueue.summaryLines.splice(0, overflowQueue.summaryLines.length - overflowQueue.cap); const enqueuedAt = Date.now(); for (const run of this.runs.values()) { if (!this.isSameRunScope(run, params.runScope) || !run.pendingQueue) continue; run.pendingQueue.lastEnqueuedAt = enqueuedAt; run.pendingQueue.debounceMs = params.settings.debounceMs ?? 500; } if (params.settings.mode === "collect") { const target = [...this.runs.entries()].findLast(([, run]) => this.isSameRunScope(run, params.runScope) && run.pendingQueue); const targetQueue = target?.[1].pendingQueue; if (target && targetQueue?.mode === "collect" && !target[1].controller.signal.aborted) { const [targetRunId] = target; targetQueue.messages.push(params.message); targetQueue.dropPolicy = params.settings.dropPolicy ?? "summarize"; targetQueue.droppedCount += overflowQueue.droppedCount; targetQueue.summaryLines.push(...overflowQueue.summaryLines); return { kind: "handled", runId: targetRunId }; } } return { kind: "enqueue", queue: { mode: params.settings.mode === "collect" ? "collect" : "followup", messages: [params.message], debounceMs: params.settings.debounceMs ?? 500, lastEnqueuedAt: enqueuedAt, dropPolicy: params.settings.dropPolicy ?? "summarize", droppedCount: overflowQueue.droppedCount, summaryLines: overflowQueue.summaryLines } }; } listPendingLocalMessages(params) { const pending = []; for (const run of this.runs.values()) { if (!this.isSameRunScope(run, params) || !run.pendingQueue) continue; run.pendingQueue.messages.forEach((message, messageIndex) => { pending.push({ run, messageIndex, message }); }); } return pending; } findQueuedSessionRunPromise(params) { let queuedAfter; for (const [runId, run] of this.runs) if (this.isSameRunScope(run, params) && !run.isBtw) { const promise = this.runPromises.get(runId); if (promise) queuedAfter = { runId, run, promise }; } return queuedAfter; } abortSessionRuns(params) { for (const [runId, run] of this.runs) if (this.isSameRunScope(run, params) && !run.isBtw && this.isAbortableRun(runId, run)) run.controller.abort(); } hasAbortableSessionRun(params) { for (const [runId, run] of this.runs) if (this.isSameRunScope(run, params) && !run.isBtw && this.isAbortableRun(runId, run)) return true; return false; } isSameRunScope(run, params) { return run.sessionKey === params.sessionKey && (params.sessionKey !== "global" || run.agentId === params.agentId); } isAbortableRun(runId, run) { return !run.lifecycleEnded || this.runPromises.has(runId); } emit(event, payload) { this.onEvent?.({ event, payload, seq: ++this.seq }); } clearPendingLifecycleError(runId) { clearTimeout(this.pendingLifecycleErrors.get(runId)); this.pendingLifecycleErrors.delete(runId); } scheduleChatError(runId, run, errorMessage) { this.clearPendingLifecycleError(runId); const timer = setTimeout(() => { this.pendingLifecycleErrors.delete(runId); this.emitChatTerminal(runId, run, "error", errorMessage, "provisional"); }, AGENT_RUN_TERMINAL_RETRY_GRACE_MS); timer.unref?.(); this.pendingLifecycleErrors.set(runId, timer); } emitChatDelta(runId, run) { const normalizedText = normalizeLiveAssistantBufferedText(run.buffer, { managedMediaUrls: [...run.managedMediaUrls] }).trim(); const projected = projectLiveAssistantBufferedText(normalizedText, { suppressLeadFragments: true }); const text = projected.text.trim(); if (!text || projected.suppress) return; const deltaPayload = resolveDeltaPayload(text, run.lastBroadcastText); if (!deltaPayload.deltaText && !deltaPayload.replace) return; run.registered = true; run.lastBroadcastText = text; this.emit("chat", { runId, sessionKey: run.sessionKey, agentId: run.agentId, state: "delta", ...deltaPayload, message: assistantChatMessage(text) }); } emitChatTerminal(runId, run, state, detail, terminalState = "final") { this.clearPendingLifecycleError(runId); if (run.terminalState === "final" || run.terminalState === terminalState) return; run.terminalState = terminalState; if (terminalState === "final") { run.markQueuedRunReady(); run.finishing = false; run.lifecycleEnded = true; } run.registered = true; run.lastBroadcastText = void 0; const projected = projectLiveAssistantBufferedText(normalizeLiveAssistantBufferedText(run.buffer, { final: true, managedMediaUrls: [...run.managedMediaUrls] }).trim(), { suppressLeadFragments: false }); const text = state === "final" && !projected.suppress ? projected.text.trim() : ""; this.emit("chat", { runId, sessionKey: run.sessionKey, agentId: run.agentId, state, ...state === "final" && detail ? { stopReason: detail } : {}, ...state === "final" && run.lifecycleYielded ? { yielded: true } : {}, ...text ? { message: assistantChatMessage(text) } : {}, ...state !== "final" && (detail || state === "aborted" && run.toolErrorSummary) ? { errorMessage: formatTuiErrorMessage(detail ?? run.toolErrorSummary) } : {} }); } projectTerminalOutcome(runId, run, metadata, options = {}) { const terminalError = metadata.error && typeof metadata.error === "object" && "message" in metadata.error ? metadata.error.message : metadata.error; const outcome = options.terminalOutcome ?? buildAgentRunTerminalOutcomeFromLifecycleEvent({ phase: metadata.phase === "error" || terminalError ? "error" : "end", data: { ...metadata, error: terminalError ? formatTuiErrorMessage(terminalError) : void 0 }, abortSignal: run.controller.signal }); const state = TUI_STATE_BY_TERMINAL_CLASSIFICATION[classifyAgentRunTerminalOutcome(outcome)]; if (!state) return false; const diagnostic = state === "aborted" ? readToolValidationErrorSummary(metadata.toolErrorSummary) : outcome.reason === "failed" && options.visibleText || outcome.error || (outcome.status === "timeout" ? "The provider timed out. Please try again." : "Agent run failed."); if (metadata.phase === "error" && !isDefinitiveRunLifecycle({ phase: "error", data: metadata })) this.scheduleChatError(runId, run, diagnostic); else this.emitChatTerminal(runId, run, state, diagnostic); return true; } ensureRunRegistered(runId, run) { if (run.registered || run.isBtw) return; run.registered = true; run.lastBroadcastText = ""; this.emit("chat", { runId, sessionKey: run.sessionKey, agentId: run.agentId, state: "delta", deltaText: "", message: assistantChatMessage("") }); } handleAgentEvent(evt) { const run = this.runs.get(evt.runId); if (!run) return; const lifecyclePhase = evt.stream === "lifecycle" && typeof evt.data?.phase === "string" ? evt.data.phase : ""; if (evt.stream !== "lifecycle" || lifecyclePhase !== "error") this.clearPendingLifecycleError(evt.runId); if (evt.stream !== "assistant") this.ensureRunRegistered(evt.runId, run); this.emit("agent", { runId: evt.runId, sessionKey: run.sessionKey, agentId: run.agentId, stream: evt.stream, data: evt.data }); if (evt.stream === "assistant" || evt.stream === "tool" && evt.data?.phase === "start") run.toolErrorSummary = void 0; else if (evt.stream === "tool" && evt.data?.phase === "result") run.toolErrorSummary = readToolValidationErrorSummary(evt.data.toolErrorSummary); const assistantLiveChatInput = evt.stream === "assistant" ? resolveAssistantLiveChatInput(evt.data) : void 0; if (assistantLiveChatInput && !run.isBtw && !shouldSuppressAssistantEventForLiveChat(evt.data)) { for (const url of assistantLiveChatInput.managedMediaUrls ?? []) run.managedMediaUrls.add(url); run.buffer = resolveMergedAssistantText({ previousText: run.buffer, nextText: assistantLiveChatInput.text, nextDelta: assistantLiveChatInput.delta }); this.emitChatDelta(evt.runId, run); return; } if (evt.stream !== "lifecycle") return; const phase = lifecyclePhase; if (phase === "finishing") { run.finishing = true; run.markQueuedRunReady(); run.lifecycleStopReason = typeof evt.data?.stopReason === "string" ? evt.data.stopReason : void 0; return; } if (phase !== "end" && phase !== "error") return; run.finishing = false; if (phase === "error") run.buffer = ""; if (this.projectTerminalOutcome(evt.runId, run, evt.data)) return; run.lifecycleEnded = true; run.markQueuedRunReady(); run.lifecycleStopReason = typeof evt.data?.stopReason === "string" ? evt.data.stopReason : void 0; run.lifecycleYielded = isAgentLifecycleYieldedWaiting(evt.data); } async runTurn(params) { try { const recheckPreparedRuntimeAtAdmission = params.queuedAfter !== void 0; if (params.queuedAfter) { try { await Promise.race([waitForQueuedLocalRun(params.queuedAfter, params.runId), waitForAbortSignal(params.controller.signal)]); } catch (error) { const run = this.runs.get(params.runId); if (run) { const errorMessage = error instanceof Error ? error.message : String(error); this.emitChatTerminal(params.runId, run, "error", `previous run did not finish cleanly: ${errorMessage}`); } return; } if (params.controller.signal.aborted) { const run = this.runs.get(params.runId); if (run) this.emitChatTerminal(params.runId, run, "aborted"); return; } } const activeRun = this.runs.get(params.runId); delete activeRun?.queuedAfter; let message = params.message; if (activeRun?.pendingQueue) { await waitForQueueDebounce(activeRun.pendingQueue, params.controller.signal); if (params.controller.signal.aborted) { this.emitChatTerminal(params.runId, activeRun, "aborted"); return; } message = buildLocalQueuedPrompt(activeRun.pendingQueue); delete activeRun.pendingQueue; } if (recheckPreparedRuntimeAtAdmission) { await this.preparedModelRuntime.waitUntilReady(); if (params.controller.signal.aborted) { if (activeRun) this.emitChatTerminal(params.runId, activeRun, "aborted"); return; } } if (activeRun?.isBtw && activeRun.question) { const result = await this.runBtwTurn({ runId: params.runId, sessionKey: params.sessionKey, ...params.agentId ? { agentId: params.agentId } : {}, question: activeRun.question, timeoutMs: params.timeoutMs, controller: params.controller }); const run = this.runs.get(params.runId); if (!run) return; if (params.controller.signal.aborted) { this.emitChatTerminal(params.runId, run, "aborted"); return; } this.emit("chat.side_result", { kind: "btw", runId: params.runId, sessionKey: result.sessionKey, agentId: run.agentId, question: run.question, text: result.text, ...result.isError ? { isError: true } : {} }); this.emitChatTerminal(params.runId, run, "final"); return; } const loadOptions = params.agentId ? { agentId: params.agentId } : void 0; const { canonicalKey, entry } = loadGatewaySessionEntry(params.sessionKey, loadOptions); const result = await agentCommandFromIngress({ message, sessionKey: canonicalKey, ...params.agentId ? { agentId: params.agentId } : {}, ...entry?.sessionId ? { sessionId: entry.sessionId } : {}, thinking: params.thinking, deliver: params.deliver, channel: INTERNAL_MESSAGE_CHANNEL, runContext: { messageChannel: INTERNAL_MESSAGE_CHANNEL }, timeout: timeoutSecondsFromMs(params.timeoutMs), runId: params.runId, abortSignal: params.controller.signal, allowModelOverride: false }, silentRuntime, this.deps); const run = this.runs.get(params.runId); if (!run) return; if (this.projectTerminalOutcome(params.runId, run, result?.meta ?? {}, { visibleText: payloadText(result?.payloads) })) return; run.lifecycleYielded ||= isAgentLifecycleYieldedWaiting({ phase: "end", ...result?.meta }); if (run.isBtw) { const text = payloadText(result?.payloads); if (run.question && text) this.emit("chat.side_result", { kind: "btw", runId: params.runId, sessionKey: run.sessionKey, agentId: run.agentId, question: run.question, text }); this.emitChatTerminal(params.runId, run, "final"); return; } if (run.terminalState !== "final") { const finalText = payloadText(result?.payloads); if (finalText) run.buffer = finalText; const stopReason = run.lifecycleStopReason ?? (typeof result?.meta?.stopReason === "string" ? result.meta.stopReason : void 0); this.emitChatTerminal(params.runId, run, "final", stopReason); } } catch (error) { const run = this.runs.get(params.runId); if (!run) return; const errorMessage = error instanceof Error ? error.message : String(error); const outcome = findAgentRunTerminalOutcome(error); this.projectTerminalOutcome(params.runId, run, outcome ?? { status: "error", error: errorMessage }, outcome ? { terminalOutcome: outcome } : {}); } finally { this.runs.get(params.runId)?.markQueuedRunReady(); this.runs.delete(params.runId); } } }; //#endregion export { EmbeddedTuiBackend };