{"version":3,"file":"channels-DpBkVl9k.cjs","names":["AgentChannels"],"sources":["../src/channels/agent-controller-channels.ts"],"sourcesContent":["import type { Message, Thread } from 'chat';\n\nimport type { Agent } from '../agent/agent';\nimport type { MastraProviderMetadata } from '../agent/message-list/state/types';\nimport type { AgentSignalContents } from '../agent/signals';\nimport type { AgentController } from '../agent-controller/agent-controller';\nimport type { Session } from '../agent-controller/session';\nimport type { Mastra } from '../mastra';\nimport type { StorageThreadType } from '../memory/types';\nimport type { RequestContext } from '../request-context';\n\nimport { AgentChannels } from './agent-channels';\nimport type { ChannelConfig } from './types';\n\n/** Configuration for {@link AgentControllerChannels}. Same shape as agent channels. */\nexport type AgentControllerChannelsConfig = ChannelConfig;\n\n/**\n * Runs an AgentController inside chat channels (Slack, Discord, ...).\n *\n * Extends {@link AgentChannels} so all inbound machinery (thread mapping,\n * history, attachments, event context) and all outbound rendering\n * (`ChatChannelOutputProcessor` with native streaming, tool cards, typing\n * status) are reused unchanged. Only the dispatch seams differ: instead of\n * routing into a bare agent, inbound messages route into a controller\n * `Session` — one durable session per chat thread, keyed by the mapped\n * Mastra thread's `resourceId`.\n *\n * V1 targets long-lived servers: controller sessions are in-memory objects\n * and do not survive process restarts.\n */\nexport class AgentControllerChannels extends AgentChannels {\n  private controller: AgentController<any> | null = null;\n\n  /**\n   * Session resourceIds whose adapter can't render approval buttons, so their\n   * runs must auto-approve tools (`requireToolApproval: false`) instead of\n   * parking forever on an approval nobody can answer. Kept outside session\n   * state on purpose: state is validated against the controller's\n   * `stateSchema`, which would strip (or reject) an injected flag. Refreshed\n   * on every inbound message; in-memory only, matching the v1 long-lived\n   * server scope.\n   */\n  private autoApproveResourceIds = new Set<string>();\n\n  /** @internal Called by AgentController's constructor to bind itself. */\n  __setController(controller: AgentController<any>): void {\n    this.controller = controller;\n  }\n\n  /**\n   * @internal Consulted by the controller's run-option builder: `true` when\n   * the session's channel adapter can't render approval buttons and tool\n   * calls must auto-approve (the session-side equivalent of the base agent\n   * path's `autoResumeSuspendedTools`).\n   */\n  __isAutoApproveResource(resourceId: string): boolean {\n    return this.autoApproveResourceIds.has(resourceId);\n  }\n\n  /**\n   * @internal No-op override. The controller attaches this instance to its\n   * mode agents via `Agent.setChannels`, which calls `__setAgent(agent)` —\n   * with multiple mode agents the last one would win. Every `this.agent` use\n   * is overridden in this subclass, so keep the base field unset rather than\n   * holding a misleading ref.\n   */\n  override __setAgent(_agent: Agent<any, any, any, any>): void {}\n\n  protected override getOwnerId(): string | null {\n    return this.controller?.id ?? null;\n  }\n\n  protected override getWebhookBasePath(): string {\n    return `/api/agent-controllers/${this.getOwnerId()}`;\n  }\n\n  protected override getMastra(): Mastra | undefined {\n    return this.controller?.getMastra();\n  }\n\n  /**\n   * One session per chat thread: unless the user supplied a custom\n   * `resolveResourceId`, key new Mastra threads (and therefore controller\n   * sessions) off the platform + external thread id.\n   */\n  protected override resolveChannelResourceId(args: {\n    platform: string;\n    chatThread: Thread;\n    message: Message;\n    defaultResourceId: string;\n  }): string | (() => string | Promise<string>) {\n    const base = super.resolveChannelResourceId(args);\n    // The base returns a thunk only when a custom resolveResourceId was\n    // configured — honor it. Otherwise derive the channel-thread key. The\n    // adapter's thread id is already platform-prefixed (e.g. `slack:C123:ts`),\n    // so don't prepend the platform again or the key double-prefixes.\n    if (typeof base === 'function') return base;\n    return `channel:${args.chatThread.id}`;\n  }\n\n  /**\n   * Route an inbound chat message into the controller session bound to this\n   * chat thread. Output renders back to the platform through the channels\n   * output processor: the `requestContext` built by the base class (carrying\n   * the channel context and render context) flows through the session into\n   * the run.\n   */\n  protected override async dispatchInboundMessage(args: {\n    signalContents: AgentSignalContents;\n    attributes: Record<string, string | undefined>;\n    providerOptions: MastraProviderMetadata;\n    requestContext: RequestContext;\n    thread: StorageThreadType;\n    memory: { thread: string; resource: string };\n    autoResumeSuspendedTools: true | undefined;\n  }): Promise<void> {\n    const { signalContents, attributes, providerOptions, requestContext, thread, autoResumeSuspendedTools } = args;\n\n    // The tenant, when there is one, is already stamped on `requestContext` by\n    // the channel handler that accepted this message — a host that maps\n    // platform senders to Mastra users writes `user` before calling\n    // `defaultHandler`, and gating an unlinked sender means not calling it at\n    // all. Core dispatches whatever reaches it.\n    const session = await this.getSessionForThread(thread, requestContext);\n\n    // The session equivalent of the base path's `autoResumeSuspendedTools`:\n    // controller runs set `requireToolApproval` from this marker, so on\n    // adapters that can't render approval buttons the run auto-approves\n    // instead of parking forever on an approval nobody can answer. Tracked\n    // outside session state so the controller's `stateSchema` (which would\n    // strip or reject an injected key) never sees it.\n    const sessionResourceId = thread.resourceId;\n    if (autoResumeSuspendedTools) {\n      this.autoApproveResourceIds.add(sessionResourceId);\n    } else {\n      this.autoApproveResourceIds.delete(sessionResourceId);\n    }\n\n    const result = session.sendSignal({\n      content: signalContents,\n      ifActive: { attributes },\n      ifIdle: { attributes },\n      requestContext,\n      providerOptions,\n    });\n    await result.accepted;\n  }\n\n  /**\n   * Resolve an approval-card \"approve\" action against the controller session's\n   * parked tool-approval gate. The run engine — awaiting the gate inside its\n   * stream-consumer loop — performs the actual resume itself and keeps\n   * consuming, so the continuation renders through the output processor.\n   */\n  protected override async dispatchApproval(args: {\n    runId: string;\n    toolCallId: string;\n    requestContext: RequestContext;\n    memory: { thread: string; resource: string };\n  }): Promise<void> {\n    await this.respondToSessionApproval({ decision: 'approve', ...args });\n  }\n\n  /**\n   * Resolve an approval-card \"deny\" action against the controller session's\n   * parked tool-approval gate (see {@link dispatchApproval}).\n   */\n  protected override async dispatchDecline(args: {\n    runId: string;\n    toolCallId: string;\n    requestContext: RequestContext;\n    memory: { thread: string; resource: string };\n  }): Promise<void> {\n    await this.respondToSessionApproval({ decision: 'decline', ...args });\n  }\n\n  /**\n   * Shared approve/decline path. Never calls the session's internal\n   * `approveToolCall`/`declineToolCall` executors directly — the engine parked\n   * at the gate owns the resume. `respondToToolApproval` is a silent no-op\n   * when nothing is armed or the toolCallId mismatches, so staleness is\n   * pre-checked explicitly (an armed gate does not survive process restarts,\n   * so restart-recovered approvals are always stale — consistent with the\n   * v1 long-lived-server scope).\n   */\n  private async respondToSessionApproval({\n    decision,\n    toolCallId,\n    requestContext,\n    memory,\n  }: {\n    decision: 'approve' | 'decline';\n    toolCallId: string;\n    requestContext: RequestContext;\n    memory: { thread: string; resource: string };\n  }): Promise<void> {\n    const session = await this.getSessionForThread({ id: memory.thread, resourceId: memory.resource });\n    if (!session.approval.isArmed() || session.approval.getToolCallId() !== toolCallId) {\n      this.log(\n        'info',\n        `Ignoring stale tool ${decision === 'approve' ? 'approval' : 'denial'} action (no matching parked approval for toolCallId=${toolCallId})`,\n      );\n      return;\n    }\n    // The requestContext carries the channel render context, so the resumed\n    // stream renders back to the platform through the output processor.\n    session.respondToToolApproval({ decision, toolCallId, requestContext });\n  }\n\n  /**\n   * Get-or-create the durable controller session for a mapped channel thread\n   * and bind it to that thread. Keyed off the thread's own `resourceId` so\n   * pre-existing threads (custom resolveResourceId, or created before this\n   * feature) always pass the session's thread-ownership check.\n   */\n  protected async getSessionForThread(\n    thread: Pick<StorageThreadType, 'id' | 'resourceId'>,\n    requestContext?: RequestContext,\n  ): Promise<Session<any>> {\n    const controller = this.requireController();\n    const channelResourceId = thread.resourceId;\n    // `createSession` is get-or-create keyed by resourceId, so follow-up messages\n    // on the same thread reuse the cached session bound to this thread. The\n    // dispatch requestContext must flow in: a dynamic workspace factory is\n    // resolved once at session creation with THIS context, and it may need the\n    // stamped tenant (`user`) to authorize a repo-backed session workspace.\n    const session = await controller.createSession({\n      resourceId: channelResourceId,\n      id: channelResourceId,\n      ownerId: controller.id,\n      requestContext,\n    });\n    // Bind the mapped thread. Guard is mandatory: `switch` aborts any active\n    // run, so never re-switch when the session is already on this thread.\n    if (session.thread.getId() !== thread.id) {\n      await session.thread.switch({ threadId: thread.id });\n    }\n    return session;\n  }\n\n  private requireController(): AgentController<any> {\n    if (!this.controller) {\n      throw new Error(\n        'AgentControllerChannels is not bound to an AgentController. Pass it via `channels` in AgentControllerConfig.',\n      );\n    }\n    return this.controller;\n  }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA+BA,IAAa,0BAAb,cAA6CA,cAAAA,cAAc;CACzD,aAAkD;;;;;;;;;;CAWlD,yCAAiC,IAAI,IAAY;;CAGjD,gBAAgB,YAAwC;EACtD,KAAK,aAAa;CACpB;;;;;;;CAQA,wBAAwB,YAA6B;EACnD,OAAO,KAAK,uBAAuB,IAAI,UAAU;CACnD;;;;;;;;CASA,WAAoB,QAAyC,CAAC;CAE9D,aAA+C;EAC7C,OAAO,KAAK,YAAY,MAAM;CAChC;CAEA,qBAAgD;EAC9C,OAAO,0BAA0B,KAAK,WAAW;CACnD;CAEA,YAAmD;EACjD,OAAO,KAAK,YAAY,UAAU;CACpC;;;;;;CAOA,yBAA4C,MAKE;EAC5C,MAAM,OAAO,MAAM,yBAAyB,IAAI;EAKhD,IAAI,OAAO,SAAS,YAAY,OAAO;EACvC,OAAO,WAAW,KAAK,WAAW;CACpC;;;;;;;;CASA,MAAyB,uBAAuB,MAQ9B;EAChB,MAAM,EAAE,gBAAgB,YAAY,iBAAiB,gBAAgB,QAAQ,6BAA6B;EAO1G,MAAM,UAAU,MAAM,KAAK,oBAAoB,QAAQ,cAAc;EAQrE,MAAM,oBAAoB,OAAO;EACjC,IAAI,0BACF,KAAK,uBAAuB,IAAI,iBAAiB;OAEjD,KAAK,uBAAuB,OAAO,iBAAiB;EAUtD,MAPe,QAAQ,WAAW;GAChC,SAAS;GACT,UAAU,EAAE,WAAW;GACvB,QAAQ,EAAE,WAAW;GACrB;GACA;EACF,CACW,CAAC,CAAC;CACf;;;;;;;CAQA,MAAyB,iBAAiB,MAKxB;EAChB,MAAM,KAAK,yBAAyB;GAAE,UAAU;GAAW,GAAG;EAAK,CAAC;CACtE;;;;;CAMA,MAAyB,gBAAgB,MAKvB;EAChB,MAAM,KAAK,yBAAyB;GAAE,UAAU;GAAW,GAAG;EAAK,CAAC;CACtE;;;;;;;;;;CAWA,MAAc,yBAAyB,EACrC,UACA,YACA,gBACA,UAMgB;EAChB,MAAM,UAAU,MAAM,KAAK,oBAAoB;GAAE,IAAI,OAAO;GAAQ,YAAY,OAAO;EAAS,CAAC;EACjG,IAAI,CAAC,QAAQ,SAAS,QAAQ,KAAK,QAAQ,SAAS,cAAc,MAAM,YAAY;GAClF,KAAK,IACH,QACA,uBAAuB,aAAa,YAAY,aAAa,SAAS,sDAAsD,WAAW,EACzI;GACA;EACF;EAGA,QAAQ,sBAAsB;GAAE;GAAU;GAAY;EAAe,CAAC;CACxE;;;;;;;CAQA,MAAgB,oBACd,QACA,gBACuB;EACvB,MAAM,aAAa,KAAK,kBAAkB;EAC1C,MAAM,oBAAoB,OAAO;EAMjC,MAAM,UAAU,MAAM,WAAW,cAAc;GAC7C,YAAY;GACZ,IAAI;GACJ,SAAS,WAAW;GACpB;EACF,CAAC;EAGD,IAAI,QAAQ,OAAO,MAAM,MAAM,OAAO,IACpC,MAAM,QAAQ,OAAO,OAAO,EAAE,UAAU,OAAO,GAAG,CAAC;EAErD,OAAO;CACT;CAEA,oBAAkD;EAChD,IAAI,CAAC,KAAK,YACR,MAAM,IAAI,MACR,8GACF;EAEF,OAAO,KAAK;CACd;AACF"}