// src/types/graph.ts import type { START, StateType, UpdateType, StateGraph, StateGraphArgs, StateDefinition, CompiledStateGraph, BinaryOperatorAggregate, } from '@langchain/langgraph'; import type { BindToolsInput } from '@langchain/core/language_models/chat_models'; import type { BaseMessage, AIMessageChunk, SystemMessage, } from '@langchain/core/messages'; import type { RunnableConfig, Runnable } from '@langchain/core/runnables'; import type { ChatGenerationChunk } from '@langchain/core/outputs'; import type { GoogleAIToolType } from '@langchain/google-common'; import type { ToolMap, ToolEndEvent, GenericTool, LCTool, ToolApprovalConfig, ToolExecuteBatchRequest, } from '@/types/tools'; import type { Providers, Callback, GraphNodeKeys } from '@/common'; import type { StandardGraph, MultiAgentGraph } from '@/graphs'; import type { ClientOptions } from '@/types/llm'; import type { RunStep, RunStepDeltaEvent, MessageDeltaEvent, ReasoningDeltaEvent, } from '@/types/stream'; import type { TokenCounter } from '@/types/run'; /** Interface for bound model with stream and invoke methods */ export interface ChatModel { stream?: ( messages: BaseMessage[], config?: RunnableConfig ) => Promise>; invoke: ( messages: BaseMessage[], config?: RunnableConfig ) => Promise; } /** Payload for ON_AGENT_TRANSITION events */ export type AgentTransitionEvent = { sourceAgentId?: string; sourceAgentName?: string; destinationAgentId: string; destinationAgentName: string; edgeType: string; // 'handoff' | 'transfer' | 'sequence' timestamp: number; /** When true, this event signals handoff completion (child → parent return) */ isCompletion?: boolean; /** Duration of child agent execution in milliseconds (only on completion events) */ durationMs?: number; /** Length of child agent result text in characters (only on completion events) */ resultLength?: number; }; export type GraphNode = GraphNodeKeys | typeof START; export type ClientCallback = ( graph: StandardGraph, ...args: T ) => void; export type ClientCallbacks = { [Callback.TOOL_ERROR]?: ClientCallback<[Error, string]>; [Callback.TOOL_START]?: ClientCallback; [Callback.TOOL_END]?: ClientCallback; }; export type SystemCallbacks = { [K in keyof ClientCallbacks]: ClientCallbacks[K] extends ClientCallback< infer Args > ? (...args: Args) => void : never; }; export type BaseGraphState = { messages: BaseMessage[]; /** * Structured response when using structured output mode. * Contains the validated JSON response conforming to the configured schema. */ structuredResponse?: Record; }; export type MultiAgentGraphState = BaseGraphState & { agentMessages?: BaseMessage[]; }; export type IState = BaseGraphState; export interface EventHandler { handle( event: string, data: | StreamEventData | ModelEndData | RunStep | RunStepDeltaEvent | MessageDeltaEvent | ReasoningDeltaEvent | SubagentUpdateEvent | ToolExecuteBatchRequest | { result: ToolEndEvent }, metadata?: Record, graph?: StandardGraph | MultiAgentGraph ): void | Promise; } export type GraphStateChannels = StateGraphArgs['channels']; export type Workflow< T extends BaseGraphState = BaseGraphState, U extends Partial = Partial, N extends string = string, > = StateGraph; export type CompiledWorkflow< T extends BaseGraphState = BaseGraphState, U extends Partial = Partial, N extends string = string, > = CompiledStateGraph; export type CompiledStateWorkflow = CompiledStateGraph< StateType<{ messages: BinaryOperatorAggregate; }>, UpdateType<{ messages: BinaryOperatorAggregate; }>, string, { messages: BinaryOperatorAggregate; }, { messages: BinaryOperatorAggregate; }, StateDefinition >; export type CompiledMultiAgentWorkflow = CompiledStateGraph< StateType<{ messages: BinaryOperatorAggregate; agentMessages: BinaryOperatorAggregate; }>, UpdateType<{ messages: BinaryOperatorAggregate; agentMessages: BinaryOperatorAggregate; }>, string, { messages: BinaryOperatorAggregate; agentMessages: BinaryOperatorAggregate; }, { messages: BinaryOperatorAggregate; agentMessages: BinaryOperatorAggregate; }, StateDefinition >; export type CompiledAgentWorfklow = CompiledStateGraph< { messages: BaseMessage[]; }, { messages?: BaseMessage[] | undefined; }, '__start__' | `agent=${string}` | `tools=${string}`, { messages: BinaryOperatorAggregate; }, { messages: BinaryOperatorAggregate; }, StateDefinition, { [x: `agent=${string}`]: Partial; // eslint-disable-next-line @typescript-eslint/no-explicit-any [x: `tools=${string}`]: any; } >; export type SystemRunnable = | Runnable< BaseMessage[], (BaseMessage | SystemMessage)[], RunnableConfig> > | undefined; /** * Optional compile options passed to workflow.compile(). * These are intentionally untyped to avoid coupling to library internals. */ export type CompileOptions = { // A checkpointer instance (e.g., MemorySaver, SQL saver) // eslint-disable-next-line @typescript-eslint/no-explicit-any checkpointer?: any; interruptBefore?: string[]; interruptAfter?: string[]; /** * Human-in-the-loop tool approval configuration. * When set, tools matching the policy will trigger an interrupt() * before execution, pausing the graph for human approval. * Requires a checkpointer to be set for interrupt/resume to work. */ toolApprovalConfig?: ToolApprovalConfig; }; export type EventStreamCallbackHandlerInput = Parameters[2] extends Omit< infer T, 'autoClose' > ? T : never; export type StreamChunk = | (ChatGenerationChunk & { message: AIMessageChunk; }) | AIMessageChunk; /** * Data associated with a StreamEvent. */ export type StreamEventData = { /** * The input passed to the runnable that generated the event. * Inputs will sometimes be available at the *START* of the runnable, and * sometimes at the *END* of the runnable. * If a runnable is able to stream its inputs, then its input by definition * won't be known until the *END* of the runnable when it has finished streaming * its inputs. */ input?: unknown; /** * The output of the runnable that generated the event. * Outputs will only be available at the *END* of the runnable. * For most runnables, this field can be inferred from the `chunk` field, * though there might be some exceptions for special cased runnables (e.g., like * chat models), which may return more information. */ output?: unknown; /** * A streaming chunk from the output that generated the event. * chunks support addition in general, and adding them up should result * in the output of the runnable that generated the event. */ chunk?: StreamChunk; /** * Runnable config for invoking other runnables within handlers. */ config?: RunnableConfig; /** * Custom result from the runnable that generated the event. */ result?: unknown; /** * Custom field to indicate the event was manually emitted, and may have been handled already */ emitted?: boolean; }; /** * A streaming event. * * Schema of a streaming event which is produced from the streamEvents method. */ export type StreamEvent = { /** * Event names are of the format: on_[runnable_type]_(start|stream|end). * * Runnable types are one of: * - llm - used by non chat models * - chat_model - used by chat models * - prompt -- e.g., ChatPromptTemplate * - tool -- LangChain tools * - chain - most Runnables are of this type * * Further, the events are categorized as one of: * - start - when the runnable starts * - stream - when the runnable is streaming * - end - when the runnable ends * * start, stream and end are associated with slightly different `data` payload. * * Please see the documentation for `EventData` for more details. */ event: string; /** The name of the runnable that generated the event. */ name: string; /** * An randomly generated ID to keep track of the execution of the given runnable. * * Each child runnable that gets invoked as part of the execution of a parent runnable * is assigned its own unique ID. */ run_id: string; /** * Tags associated with the runnable that generated this event. * Tags are always inherited from parent runnables. */ tags?: string[]; /** Metadata associated with the runnable that generated this event. */ metadata: Record; /** * Event data. * * The contents of the event data depend on the event type. */ data: StreamEventData; }; export type GraphConfig = { provider: string; thread_id?: string; run_id?: string; }; export type PartMetadata = { progress?: number; asset_pointer?: string; status?: string; action?: boolean; output?: string; }; export type ModelEndData = | (StreamEventData & { output: AIMessageChunk | undefined }) | undefined; export type GraphTools = GenericTool[] | BindToolsInput[] | GoogleAIToolType[]; export type StandardGraphInput = { runId?: string; signal?: AbortSignal; agents: AgentInputs[]; tokenCounter?: TokenCounter; indexTokenCountMap?: Record; }; /** * Configuration for an approval gate placed on a sequence edge. * When present, the graph inserts an approval gate node between the source * and destination agents. The gate ALWAYS fires (regardless of ExecutionContext) * and calls interrupt() to pause the graph for human approval. */ export type ApprovalGateConfig = { /** Unique identifier for this gate (used as node ID suffix) */ gateId: string; /** * Approval channel — where the approval UI is rendered. * - 'chat': SSE-based chat UI (default) * - 'outlook': MS Graph Actionable Messages * - 'telegram': Telegram Bot inline keyboard */ channel?: 'chat' | 'outlook' | 'telegram'; /** Optional human-readable prompt shown to the approver */ prompt?: string; /** Optional approver identifier (e.g., email, user ID) */ approver?: string; /** Timeout in ms before the gate auto-expires (default: 5 minutes) */ timeoutMs?: number; /** What to do on denial: 'stop' ends the graph, 'skip' skips the destination agent */ onDeny?: 'stop' | 'skip'; }; export type GraphEdge = { /** Agent ID, use a list for multiple sources */ from: string | string[]; /** Agent ID, use a list for multiple destinations */ to: string | string[]; description?: string; /** Can return boolean or specific destination(s) */ condition?: (state: BaseGraphState) => boolean | string | string[]; /** * EdgeType.HANDOFF — one-way routing: parent emits an `lc_transfer_to_*` * tool call and exits, child takes over and responds directly to the * user. Aligns with upstream's handoff semantics. * EdgeType.DIRECT — fixed graph edges for automatic sequential / parallel * transitions. Ranger preserves its enriched wiring (fan-in with prompt, * parallel groups, ApprovalGateNode, excludeResults / agentMessages). */ edgeType?: import('@/common').EdgeType; /** * For sequence edges: Optional prompt to add when transitioning through this edge. * String prompts can include variables like {results} which will be replaced with * messages from startIndex onwards. When {results} is used, excludeResults defaults to true. * * For transfer edges: Description for the input parameter that the transfer tool accepts, * allowing the supervisor to pass specific instructions/context to the transferred agent. */ prompt?: | string | (( messages: BaseMessage[], runStartIndex: number ) => string | Promise | undefined); /** * When true, excludes messages from startIndex when adding prompt. * Automatically set to true when {results} variable is used in prompt. */ excludeResults?: boolean; /** * For transfer edges: Customizes the parameter name for the transfer input. * Defaults to "instructions" if not specified. * Only applies when prompt is provided for transfer edges. * * For handoff edges: Customizes the parameter name for the handoff instruction input. */ promptKey?: string; /** * Approval gate configuration for sequence edges. * When set, inserts an approval gate node between source and destination. * The gate ALWAYS fires regardless of ExecutionContext (unlike tool approval). */ approvalGate?: ApprovalGateConfig; }; export type MultiAgentGraphInput = StandardGraphInput & { edges: GraphEdge[]; /** * When set, the graph routes START to this agent instead of the default * starting nodes. Used for multi-turn resumption: the caller reads the * last active agent id from the previous turn (e.g. via * `Run.getLastActiveAgentId()`, which derives it from the run's content * data trail) and passes it here so follow-up messages route to the * agent that last handled the conversation. * * If the agent ID is invalid (not in the graph), falls back to default * starting nodes. */ resumeFromAgentId?: string; }; /** * Structured output mode determines how the agent returns structured data. * - 'tool': Uses tool calling to return structured output (works with all tool-calling models) * - 'provider': Uses provider-native structured output via LangChain's jsonMode (OpenAI, Anthropic, etc.) * - 'native': Uses provider's constrained decoding API directly for guaranteed schema compliance * (Anthropic output_config.format, OpenAI response_format.json_schema). Falls back to 'tool' for unsupported providers. * - 'auto': Automatically selects the best strategy — 'native' for supported providers, 'tool' for others */ export type StructuredOutputMode = 'tool' | 'provider' | 'native' | 'auto'; /** * Resolved method used internally after mode resolution. * Maps to LangChain's withStructuredOutput method parameter plus our native path. */ export type ResolvedStructuredOutputMethod = | 'functionCalling' | 'jsonMode' | 'jsonSchema' | 'native' | undefined; /** * Error thrown when the model refuses to produce structured output due to safety policies. */ export class StructuredOutputRefusalError extends Error { constructor(public refusalText: string) { super(`Model refused to produce structured output: ${refusalText}`); this.name = 'StructuredOutputRefusalError'; } } /** * Error thrown when the structured output response was truncated due to max_tokens. */ export class StructuredOutputTruncatedError extends Error { constructor(public stopReason: string) { super( `Structured output was truncated (stop_reason: ${stopReason}). ` + 'Increase max_tokens to allow the full JSON response to be generated.' ); this.name = 'StructuredOutputTruncatedError'; } } /** * Configuration for structured JSON output from agents. * When configured, the agent will return a validated JSON response * instead of streaming text. */ export interface StructuredOutputConfig { /** * JSON Schema defining the output structure. * The model will be forced to return data conforming to this schema. */ schema: Record; /** * Name for the structured output format (used in tool mode). * @default 'StructuredResponse' */ name?: string; /** * Description of what the structured output represents. * Helps the model understand the expected format. */ description?: string; /** * Output mode strategy. * @default 'auto' */ mode?: StructuredOutputMode; /** * Enable strict schema validation. * When true, the response must exactly match the schema. * @default true */ strict?: boolean; /** * Error handling configuration. * - true: Auto-retry on validation errors (default) * - false: Throw error on validation failure * - string: Custom error message for retry */ handleErrors?: boolean | string; /** * Maximum number of retry attempts on validation failure. * @default 2 */ maxRetries?: number; /** * Include the raw AI message along with structured response. * Useful for debugging. * @default false */ includeRaw?: boolean; } /** * Database/API structured output format (snake_case with enabled flag). * This matches the format stored in MongoDB and sent from frontends. */ export interface StructuredOutputInput { /** Whether structured output is enabled */ enabled?: boolean; /** JSON Schema defining the expected response structure */ schema?: Record; /** Name identifier for the structured output */ name?: string; /** Description of what the structured output represents */ description?: string; /** Mode for structured output: 'tool' | 'provider' | 'native' | 'auto' */ mode?: StructuredOutputMode; /** Whether to enforce strict schema validation */ strict?: boolean; } /** * Trigger strategy for when summarization should activate. * - 'contextPercentage': Trigger when context utilization exceeds a threshold percentage * - 'messageCount': Trigger when pruned message count exceeds a threshold * - 'tokenThreshold': Trigger when total token count exceeds a raw threshold */ export type SummarizationTriggerType = | 'contextPercentage' | 'messageCount' | 'tokenThreshold'; /** * Configuration for summarization behavior within the agent pipeline. * All fields are optional — sensible defaults are provided via constants. * * @see SUMMARIZATION_CONTEXT_THRESHOLD, SUMMARIZATION_RESERVE_RATIO, PRUNING_EMA_ALPHA */ export interface SummarizationConfig { /** * Strategy for when summarization triggers. * @default 'contextPercentage' */ triggerType?: SummarizationTriggerType; /** * Threshold value interpreted based on triggerType: * - contextPercentage: 0-100 (percentage of context window) * - messageCount: absolute count of messages pruned * - tokenThreshold: absolute token count * @default 80 (for contextPercentage) */ triggerThreshold?: number; /** * Fraction of context window (0-1) reserved for recent messages. * Prevents over-pruning by ensuring at least this fraction of the * context budget is preserved as recent conversation history. * @default 0.3 */ reserveRatio?: number; /** * Whether context pruning is enabled (can be disabled for debugging). * @default true */ contextPruning?: boolean; /** * Initial summary text to seed across runs. * Different from persistedSummary: this is provided by the caller as a * cross-conversation seed (e.g., agent personality or recurring context), * while persistedSummary is loaded from the conversation's own history. */ initialSummary?: string; /** * Upstream-aligned optional fields. When the host wants the summarization * pass to run on a different LLM than the agent's own (e.g. a cheaper * model for compaction), these provider/model overrides flow through to * the summarize callback. */ provider?: Providers; model?: string; parameters?: Record; prompt?: string; updatePrompt?: string; trigger?: { type: string; value: number }; maxSummaryTokens?: number; } /** * Runtime state for EMA-based pruning calibration. * Maintained across iterations within a single run to smooth pruning decisions. */ export interface PruneCalibrationState { /** Current EMA calibration ratio */ ratio: number; /** Number of calibration updates applied */ iterations: number; } /** * Lightweight file metadata entry for conversation-level file awareness. * Contains only IDs and names — NOT full content — so the agent always knows * what files exist in the conversation even after compaction pushes old messages * behind the summary window. The agent can retrieve full content on-demand * via file_search (RAG) or content_tool read (by contentId). */ export interface FileManifestEntry { /** Unique file identifier (e.g., MongoDB ObjectId or UUID) */ fileId: string; /** Original filename (e.g., "quarterly-report.pdf") */ filename: string; /** Content identifier for on-demand retrieval via content_tool read */ contentId?: string; /** File source (e.g., "local", "sharepoint", "onedrive") */ source?: string; /** Index of the message that introduced this file (0-based in the original message array) */ messageIndex?: number; } /** Configuration for a subagent type that can be spawned by a parent agent. */ export type SubagentConfig = { /** Identifier used in the tool's `subagent_type` enum (e.g. 'researcher', 'coder'). */ type: string; /** Human-readable display name. */ name: string; /** What this subagent specializes in — shown to the LLM. */ description: string; /** Full agent config for the child graph. Omit when `self` is true. */ agentInputs?: AgentInputs; /** When true, reuse the parent's AgentInputs (context isolation without separate config). */ self?: boolean; /** Max AGENT→TOOLS cycles before forced stop (default: 25). */ maxTurns?: number; /** Allow this subagent to spawn its own subagents (default: false). */ allowNested?: boolean; }; /** SubagentConfig with agentInputs guaranteed present (self-spawn resolved). */ export type ResolvedSubagentConfig = SubagentConfig & { agentInputs: AgentInputs; }; /** Lifecycle phase carried on {@link SubagentUpdateEvent}. */ export type SubagentUpdatePhase = | 'start' | 'run_step' | 'run_step_delta' | 'run_step_completed' | 'message_delta' | 'reasoning_delta' | 'stop' | 'error'; /** * Wrapper event emitted when a subagent's child graph dispatches activity. * Lets hosts show subagent progress in a UI surface separate from the parent * conversation without having to untangle events by agent ID. */ export interface SubagentUpdateEvent { /** Parent run ID. */ runId: string; /** Child run ID (unique per subagent execution). */ subagentRunId: string; /** * Parent-side `tool_call_id` for the `subagent` tool invocation that * triggered this run. Stable for the duration of the child; lets hosts * correlate updates deterministically instead of inferring by ordering. * Omitted when the executor was invoked outside of a tool-call context. */ parentToolCallId?: string; /** Subagent `type` identifier from the SubagentConfig. */ subagentType: string; /** Child agent ID assigned to this subagent execution. */ subagentAgentId: string; /** Parent agent ID that spawned this subagent. */ parentAgentId?: string; /** Lifecycle phase carried by this update. */ phase: SubagentUpdatePhase; /** Underlying event payload (shape depends on phase). */ data?: unknown; /** Short human-readable description. Hosts can render this directly. */ label?: string; /** ISO timestamp for ordering / display. */ timestamp: string; } export interface AgentInputs { agentId: string; /** Human-readable name for the agent (used in handoff context). Defaults to agentId if not provided. */ name?: string; /** Description of what this agent does (used to enrich handoff tool descriptions). */ description?: string; toolEnd?: boolean; toolMap?: ToolMap; tools?: GraphTools; provider: Providers; instructions?: string; streamBuffer?: number; maxContextTokens?: number; clientOptions?: ClientOptions; additional_instructions?: string; reasoningKey?: 'reasoning_content' | 'reasoning'; /** * Subagent types this agent may spawn. When non-empty, Graph injects the * `subagent` tool into the agent's toolset with the per-config enum and * description populated. */ subagentConfigs?: SubagentConfig[]; /** * Maximum subagent depth allowed below this agent. Used by SubagentExecutor * to abort runaway nesting. Defaults to a small constant. */ maxSubagentDepth?: number; /** * Pre-existing summary injected into the system message via * AgentContext.buildInstructionsString. Populated by SubagentExecutor when * spawning a self-config subagent that should inherit the parent's prior * compaction summary; otherwise host-supplied at run time. */ initialSummary?: { text: string; tokenCount: number }; /** * Upstream-aligned opt-in for summarization. When false, the agent never * invokes the summarize callback regardless of context utilization. * Defaults to undefined (treated as enabled in Ranger to preserve prior * behaviour); hosts that want strict opt-in semantics should set it * explicitly. */ summarizationEnabled?: boolean; /** Format content blocks as strings (for legacy compatibility i.e. Ollama/Azure Serverless) */ useLegacyContent?: boolean; /** * Tool definitions for all tools, including deferred and programmatic. * Used for tool search and programmatic tool calling. * Maps tool name to LCTool definition. */ toolRegistry?: Map; /** * Dynamic context that changes per-request (e.g., current time, user info). * This is injected as a user message rather than system prompt to preserve cache. * Keeping this separate from instructions ensures the system message stays static * and can be cached by Bedrock/Anthropic prompt caching. */ dynamicContext?: string; /** * Structured output configuration (camelCase). * When set, disables streaming and returns a validated JSON response * conforming to the specified schema. */ structuredOutput?: StructuredOutputConfig; /** * Structured output configuration (snake_case - database/API format). * Alternative to structuredOutput for compatibility with MongoDB/frontend. * Uses an `enabled` flag to control activation. * @deprecated Use structuredOutput instead when possible */ structured_output?: StructuredOutputInput; /** * Serializable tool definitions for event-driven execution. * When provided, ToolNode operates in event-driven mode, dispatching * ON_TOOL_EXECUTE events instead of invoking tools directly. */ toolDefinitions?: LCTool[]; /** * Tool names discovered from previous conversation history. * These tools will be pre-marked as discovered so they're included * in tool binding without requiring tool_search. */ discoveredTools?: string[]; /** * Optional callback for summarizing messages that were pruned from context. * When provided, discarded messages are summarized by the host caller * using a cheap LLM call, and the summary is prepended to the context. */ summarizeCallback?: ( messagesToRefine: import('@langchain/core/messages').BaseMessage[] ) => Promise; /** * Pre-existing summary text loaded from persistent storage (MongoDB/Redis). * When provided, this summary is injected into the initial message context * so the agent has prior conversation history even on new turns. * Set by the host's summary store when resuming a conversation. */ persistedSummary?: string; /** * Summarization configuration controlling trigger strategy, reserve ratio, * and EMA calibration for pruning. When omitted, sensible defaults apply. * @see SummarizationConfig */ summarizationConfig?: SummarizationConfig; /** * Workspace-shared system-message tiers. Each entry becomes a separate * text block in the SystemMessage with its own cachePoint / cache_control * marker, allowing the platform-tier bytes (e.g. shared branding, tool * routing rules, code-executor instructions) to be hashed independently * from the per-agent `instructions` block. Hosts that don't use tiered * caching can leave this undefined; behavior falls back to the legacy * single SystemMessage. * * Anthropic / Bedrock prompt-cache lookups are forward-prefix-hash, so * blocks are emitted in array order BEFORE `instructions`. Up to 4 blocks * are supported (the LLM cap on cache breakpoints). */ system_cache_blocks?: Array<{ text: string; ttl?: '5m' | '1h' }>; /** * TTL hint for the per-agent `instructions` block's cache marker. * Defaults to '5m' (matching addCacheControl message-level behavior). * Only consulted when `system_cache_blocks` is set; otherwise the * legacy SystemMessage path is unchanged. */ instructions_cache_ttl?: '5m' | '1h'; /** * Lightweight file manifest for the conversation. * Contains file IDs, names, and metadata — NOT full content. * * Used by the compaction engine to inject a [Conversation Files] block * into the windowed view, ensuring the LLM always knows what files exist * even when old messages (with full file content) are behind the summary. * * The agent can retrieve full content on-demand via: * - file_search (RAG semantic search over embedded files) * - content_tool read (by contentId for exact file retrieval) * * Built by the host orchestrator from message_file_map * and metadata.context_files across all conversation messages. */ fileManifest?: FileManifestEntry[]; } /** * Tunable knobs for position-based content degradation. * See `messages/contextPruning.ts` and `messages/contextPruningSettings.ts`. */ export interface ContextPruningConfig { enabled?: boolean; keepLastAssistants?: number; softTrimRatio?: number; hardClearRatio?: number; minPrunableToolChars?: number; softTrim?: { maxChars?: number; headChars?: number; tailChars?: number; }; hardClear?: { enabled?: boolean; placeholder?: string; }; }