import { SmrtObjectOptions } from '@happyvertical/smrt-core'; import { AgentSession } from '../models/AgentSession.js'; import { ChatMessage } from '../models/ChatMessage.js'; import { ChatThread } from '../models/ChatThread.js'; import { ChatMessageType, ChatParticipantRole, ChatRoomStatus, ChatRoomType } from '../types.js'; /** * Parameters for emitting an agent-authored (assistant/tool) message. Used only * by the internal {@link sendAgentReply} bridge below — NOT a public surface. */ export interface AgentReplyParams { tenantId: string; agentSessionId: string; content: string; kind?: 'assistant' | 'tool'; messageType?: ChatMessageType; toolCallData?: Record | null; /** * Optional thread to attach the agent reply to. It MUST belong to the same * room/tenant as the session (validated in {@link ChatService.writeMessage}). */ threadId?: string | null; } /** Tenant-bound agent session lookup descriptor for the read facade. */ export interface AgentSessionLookup { agentSessionId: string; tenantId: string | null; } /** Tenant-bound thread lookup descriptor for the read facade. */ export interface ThreadLookup { threadId: string; tenantId: string; } /** * Module-private bridge key for the agent-reply path (S5 #1392). * * The agent-authoring path is reachable only through the {@link sendAgentReply} * function (re-exported solely from `./internal/agent-runtime`). To let that * module-local function reach the `private` {@link ChatService.emitAgentReply} * without widening the class's PUBLIC surface, the class exposes a single * symbol-keyed static. A symbol key is not a named member: it does not appear on * the `ChatService` type, is not enumerable, and is callable only by code that * holds this non-exported symbol. This replaces the previous public * `_runAgentReply` static, which any consumer of the root-exported `ChatService` * could call to author messages as the agent. */ declare const RUN_AGENT_REPLY: unique symbol; export declare class ChatService { #private; private constructor(); static create(options: SmrtObjectOptions): Promise; /** Initialize all underlying collections (table creation) */ initialize(): Promise; /** * Create a room and add the creating actor as owner (S5 #1392). * * The acting identity is the server-supplied `actorProfileId` (the * authenticated principal the route injects). The creator/owner is ALWAYS the * actor — a caller cannot supply a `createdByProfileId` to attribute the room * to (and enroll as owner) some other profile. */ createRoom(params: { tenantId: string; name: string; roomType: ChatRoomType; actorProfileId: string; description?: string; topic?: string; }): Promise; /** * Send a USER message to a room as the authenticated caller (S5 #1392). * * The acting identity is the server-supplied `actorProfileId` (the * authenticated principal the route injects). The message is ALWAYS authored * as `actorProfileId` with `role: 'user'` — the caller cannot supply a * `senderProfileId` to impersonate another profile or the agent, and cannot * supply a privileged `role` (assistant/system/tool). Agent-authored messages * go exclusively through the internal {@link ChatService.sendAgentReply}. * * Authorization: `actorProfileId` must be an ACTIVE participant of the target * room, preventing cross-room IDOR within a tenant. There is no public * membership-skip parameter; system-authored writes use the internal * {@link ChatService.writeMessage} path. */ sendMessage(params: { tenantId: string; roomId: string; actorProfileId: string; content: string; messageType?: ChatMessageType; threadId?: string | null; agentSessionId?: string | null; replyToMessageId?: string | null; }): Promise; /** * Start a thread in a room (S5 #1392). * * The acting identity is the server-supplied `actorProfileId`, which must be * an active member of the room. Generated thread `create` is disabled, so this * is the only path to create a thread. * * When a `rootMessageId` is supplied it is bound to `{ id, roomId, tenantId }` * and rejected unless it belongs to the SAME room and tenant — without this a * member of one room could anchor a thread to a message from another * room/tenant. `rootMessageId` is optional (a thread can be opened without a * root message, e.g. an agent-editor thread). */ startThread(params: { tenantId: string; roomId: string; actorProfileId: string; rootMessageId?: string | null; title?: string; }): Promise; /** * Add a participant to a room (S5 #1392). * * Authorization: the acting identity is the server-supplied `actorProfileId`, * which MUST be an owner/admin of the target room. This prevents an arbitrary * tenant member from adding anyone (or themselves) to any room with any role — * a privilege-escalation / IDOR. System-bootstrap enrollment (room creation, * DM/agent-session setup) uses the internal {@link ChatService.enrollParticipant}. */ addParticipant(params: { tenantId: string; roomId: string; actorProfileId: string; profileId: string; role?: ChatParticipantRole; }): Promise; /** * Remove (soft-leave) a participant from a room (S5 #1392). * * Authorization: the server-supplied `actorProfileId` may remove THEMSELVES * (leave) at any time; removing ANOTHER profile requires the actor to be an * owner/admin of the room. An admin who is not an owner cannot remove an owner. */ removeParticipant(params: { tenantId: string; roomId: string; actorProfileId: string; profileId: string; }): Promise; /** * Update mutable room fields, restricted to a room owner/admin (S5 #1392). * * Generated `update` on ChatRoom is disabled, so this owner-checked path is the * only way to mutate room state. The acting identity is the server-supplied * `actorProfileId`. */ updateRoom(params: { tenantId: string; roomId: string; actorProfileId: string; name?: string; description?: string; topic?: string; avatarUrl?: string; status?: ChatRoomStatus; }): Promise; /** * Add a reaction to a message as the authenticated caller (S5 #1392). * * Generated `create` on ChatReaction is disabled. The reaction is always * authored as the server-supplied `actorProfileId` (no caller-supplied * `profileId`), and the actor must be an active member of the room that owns * the message. Idempotent: re-reacting with the same emoji returns the * existing row. */ addReaction(params: { tenantId: string; messageId: string; actorProfileId: string; emoji: string; }): Promise; /** * Remove the caller's own reaction from a message (S5 #1392). * * Generated `delete` on ChatReaction is disabled. A caller may only delete * THEIR OWN reaction (keyed on `actorProfileId`), so the route cannot remove * another member's reaction. */ removeReaction(params: { tenantId: string; messageId: string; actorProfileId: string; emoji: string; }): Promise; /** * Get or create a DM room with auto-participant setup. * * The acting identity is the server-supplied `actorProfileId`, which must be * one of the two DM participants — a caller cannot open a DM between two other * profiles on their behalf (S5 #1392). Enrollment uses the internal system * path (no owner check needed for a DM the actor is part of). */ getOrCreateDM(params: { tenantId: string; actorProfileId: string; profileId1: string; profileId2: string; }): Promise; /** * Create an agent conversation session with a linked chat room (S5 #1392). * * The acting identity is the server-supplied `actorProfileId`; the session is * ALWAYS created for that actor as the owning participant. A caller cannot * supply a `participantProfileId` to open (and own) a session on behalf of * another profile. * * `sessionKey` scopes the session's identity to a conversation subject (e.g. a * content id) (S5 #1392). The reuse lookup keys on `(agentId, * participantProfileId, tenantId)`, which is too coarse for callers that open * separate conversations for distinct subjects under one agent/profile/tenant: * without a key, a session opened for subject A would be reused for a request * about subject B, returning A's room/threads on B's route. When `sessionKey` * is set, an existing session is reused ONLY if its key matches exactly, and a * newly created session records the key; distinct keys therefore get distinct * sessions and rooms. When omitted, behavior is unchanged (single session per * agent/profile/tenant). */ createAgentSession(params: { tenantId: string; agentId: string; actorProfileId: string; allowedTools?: string[]; systemPrompt?: string; maxTokens?: number; maxMessages?: number; sessionKey?: string | null; }): Promise<{ session: AgentSession; room: import('../index.js').ChatRoom; }>; /** * Send a USER message within an agent session (S5 #1392). * * The authenticated caller (`actorProfileId`) must be the session's owning * participant. The message is always authored as `session.participantProfileId` * — the caller cannot supply a `senderProfileId`, a `role`, or tool-call data, * so this path can never be used to post as the agent (`assistant`/`tool`) or * to impersonate another profile. Agent replies go through the internal * {@link ChatService.sendAgentReply}. */ sendAgentUserMessage(params: { tenantId: string; agentSessionId: string; actorProfileId: string; content: string; messageType?: ChatMessageType; }): Promise; /** * Read messages in a room, gated on the AUTHENTICATED CALLER's active * membership (S5 #1392). * * The acting identity is the server-supplied `actorProfileId` (the * authenticated principal the route injects), NOT a caller-controlled * `profileId`. Authorizing a supplied `profileId` would make a route a * confused deputy: any caller could read a room by smuggling some member's * profile id. Throws if `actorProfileId` is not an active participant of * `roomId`. `tenantId` is required so the membership gate is always * tenant-scoped. */ getRoomMessages(params: { roomId: string; actorProfileId: string; tenantId: string; limit?: number; before?: string; }): Promise; /** * Load a room only if the AUTHENTICATED CALLER is an active member (S5 #1392). * * The acting identity is the server-supplied `actorProfileId`, never a * caller-controlled `profileId` (confused-deputy avoidance — see * {@link ChatService.getRoomMessages}). Returns null when the room does not * exist; throws when the actor is not an active participant. `tenantId` is * required so the lookup and the membership gate are always tenant-scoped. */ getRoomForMember(roomId: string, actorProfileId: string, tenantId: string): Promise; /** * Tenant-bound read of a single agent session (S5 #1392). * * The lookup ALWAYS binds `tenantId` (including the `null`/untenanted scope), * so a caller can never resolve a session belonging to another tenant by id. * Returns `null` when no session matches the id within the tenant. Replaces * direct `agentSessions.get(id)` reach-ins in package consumers; consumers * still apply their own ownership/context authorization on the returned row. */ getAgentSession(lookup: AgentSessionLookup): Promise; /** * Tenant-bound list of ACTIVE agent sessions for an (agent, participant) pair * (S5 #1392). * * Binds `tenantId` into the query so the result can never include a session * from another tenant. Replaces direct `agentSessions.list({ where })` * reach-ins; consumers apply their own per-session context authorization. */ findActiveAgentSessions(params: { tenantId: string | null; agentId: string; participantProfileId: string; }): Promise; /** * Tenant-bound read of a single thread (S5 #1392). * * Binds `tenantId` into the lookup so a thread from another tenant can never * be resolved by id. Returns `null` when no thread matches within the tenant. * Replaces direct `threads.get(id)` reach-ins. */ getThread(lookup: ThreadLookup): Promise; /** * List a room's threads, gated on the caller's active membership (S5 #1392). * * Tenant- and membership-scoped: throws if `actorProfileId` is not an active * participant of `roomId`. Replaces direct `threads.list({ where: { roomId } })` * reach-ins that returned threads without a membership/tenant gate. */ listRoomThreads(params: { roomId: string; actorProfileId: string; tenantId: string; }): Promise; /** * Read messages within a thread, tenant- and membership-bound (S5 #1392). * * The thread is resolved tenant-bound; the caller must be an active member of * the thread's room. Messages are returned oldest-first (chronological). * Replaces direct `messages.list({ where: { threadId } })` reach-ins that * could read another tenant's/room's thread history by raw id. */ getThreadMessages(params: { threadId: string; actorProfileId: string; tenantId: string; limit?: number; }): Promise; /** * Update the per-session agent configuration (allowedTools / systemPrompt), * restricted to the session owner — the room owner participant (S5 #1392). * * Tool whitelist and system prompt govern what the agent may do, so only the * owning participant (not arbitrary tenant members or the agent itself) may * mutate them. */ updateAgentSessionConfig(params: { agentSessionId: string; actorProfileId: string; tenantId: string | null; allowedTools?: string[]; systemPrompt?: string; }): Promise; /** * Symbol-keyed bridge to the `private` {@link ChatService.emitAgentReply} for * the module-local {@link sendAgentReply} function (S5 #1392). A static member * may reach a private instance member of its own class, so this is the * sanctioned "friend" access without widening the public instance surface. * * Keyed on the module-private {@link RUN_AGENT_REPLY} symbol — NOT a named * static — so it does not appear on the `ChatService` type, is not enumerable, * and is callable only by code holding the (non-exported) symbol. This is the * sole path the agent-runtime bridge uses to author a message as the agent. */ static [RUN_AGENT_REPLY](service: AgentReplyService, params: AgentReplyParams): Promise; } /** * Structural ChatService surface accepted by {@link sendAgentReply}. The chat * collections are `#private`, so they cannot appear in a `Pick`; this opaque * marker type keeps the agent-runtime bridge callable across module-instance * boundaries (in a pnpm workspace a consumer's `ChatService` type may resolve to * the package source while this function resolves to dist) without naming any * private member. Any real ChatService instance satisfies it. */ export type AgentReplyService = Pick; /** * Internal agent-runtime entry point: emit an ASSISTANT/TOOL message authored * as the session's agent (S5 #1392). * * NOT re-exported from `src/index.ts`. Only in-process, trusted agent-runtime * code that imports this module path directly can author messages as the agent; * route handlers and package consumers (which import from the package index) * cannot. The author is always `session.agentId`, never a caller-supplied * sender/role, and tool calls are gated fail-closed against `allowedTools`. * * Because this lives in the same module as {@link ChatService}, reaching the * symbol-keyed bridge (and through it the `private` method) is the owning * module's sanctioned "friend" access, not an external private reach-in. */ export declare function sendAgentReply(service: AgentReplyService, params: AgentReplyParams): Promise; export {}; //# sourceMappingURL=ChatService.d.ts.map