From c871455ff9d959117b9a60cb7f1fdecc251c5dda Mon Sep 17 00:00:00 2001 From: Petr Krejcik Date: Wed, 27 May 2026 12:24:21 +0200 Subject: [PATCH 1/7] add `read` property --- .../agent-builder-common/chat/conversation.ts | 5 ++ .../common/http_api/conversations.ts | 5 ++ .../server/routes/internal/conversations.ts | 49 ++++++++++++++++++- .../services/conversation/client/client.ts | 2 +- .../conversation/client/converters.ts | 6 +++ .../services/conversation/client/storage.ts | 2 + .../services/conversation/client/types.ts | 2 +- .../services/execution/utils/conversations.ts | 2 + 8 files changed, 70 insertions(+), 3 deletions(-) diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts index 2687df38131a9..2a4e73022820d 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts @@ -345,6 +345,11 @@ export interface Conversation { * Keeps track of which prompts have been answered and the response. */ state?: ConversationInternalState; + /** + * Whether the conversation has been marked as read. + * Any new or updated conversation has `read` set to `false` by default + */ + read?: boolean; } export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; diff --git a/x-pack/platform/plugins/shared/agent_builder/common/http_api/conversations.ts b/x-pack/platform/plugins/shared/agent_builder/common/http_api/conversations.ts index dff59118c29bc..0979ebe2f6dcb 100644 --- a/x-pack/platform/plugins/shared/agent_builder/common/http_api/conversations.ts +++ b/x-pack/platform/plugins/shared/agent_builder/common/http_api/conversations.ts @@ -19,3 +19,8 @@ export interface RenameConversationResponse { id: string; title: string; } + +export interface MarkReadConversationResponse { + id: string; + read: boolean; +} diff --git a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts index eff67edc90094..08a244cd2a1ed 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts @@ -8,7 +8,10 @@ import { schema } from '@kbn/config-schema'; import type { RouteDependencies } from '../types'; import { getHandlerWrapper } from '../wrap_handler'; -import type { RenameConversationResponse } from '../../../common/http_api/conversations'; +import type { + MarkReadConversationResponse, + RenameConversationResponse, +} from '../../../common/http_api/conversations'; import { apiPrivileges } from '../../../common/features'; import { internalApiPath } from '../../../common/constants'; @@ -55,4 +58,48 @@ export function registerInternalConversationRoutes({ }); }) ); + + router.post( + { + path: `${internalApiPath}/conversations/{conversation_id}/_mark_read`, + validate: { + params: schema.object({ + conversation_id: schema.string(), + }), + body: schema.object({ + read: schema.boolean(), + }), + }, + options: { access: 'internal' }, + security: { + authz: { requiredPrivileges: [apiPrivileges.readAgentBuilder] }, + }, + }, + wrapHandler(async (ctx, request, response) => { + const { conversations: conversationsService } = getInternalServices(); + const { conversation_id: conversationId } = request.params; + const { read } = request.body; + + const client = await conversationsService.getScopedClient({ request }); + const updatedConversation = await client.update({ + id: conversationId, + read, + }); + + // Do this validation in order to be able to return `read` that is always defined instead of an optional `read` which is in the Conversation type. + if (read !== updatedConversation.read) { + // TODO: check what error to throw, there are also x-pack/platform/packages/shared/agent-builder/agent-builder-common/base/errors.ts + throw new Error( + `Failed to persist read state for conversation ${conversationId}: expected ${read}, got ${updatedConversation.read}` + ); + } + + return response.ok({ + body: { + id: updatedConversation.id, + read: updatedConversation.read, + }, + }); + }) + ); } diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts index 2084b60d08176..eecc49d2bc265 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts @@ -79,7 +79,7 @@ class ConversationClientImpl implements ConversationClient { const response = await this.storage.getClient().search({ track_total_hits: false, size: 1000, - _source: ['agent_id', 'user_id', 'user_name', 'title', 'created_at', 'updated_at'], + _source: ['agent_id', 'user_id', 'user_name', 'title', 'created_at', 'updated_at', 'read'], query: { bool: { filter: [createSpaceDslFilter(this.space)], diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts index ff872edf039ab..34a9f4e7bfb25 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts @@ -59,6 +59,7 @@ const convertBaseFromEs = (document: Document) => { title: document._source.title, created_at: document._source.created_at, updated_at: document._source.updated_at, + ...(document._source.read !== undefined && { read: document._source.read }), }; }; @@ -193,6 +194,7 @@ export const fromEs = (document: Document): Conversation => { ...base, rounds: roundsWithRefs, attachments: existingAttachments, + ...(document._source!.read !== undefined && { read: document._source!.read }), ...(document._source!.state && { state: document._source!.state }), }; } @@ -202,6 +204,7 @@ export const fromEs = (document: Document): Conversation => { ...base, rounds: roundsWithRefs, ...(attachmentsForRefs.length > 0 && { attachments: attachmentsForRefs }), + ...(document._source!.read !== undefined && { read: document._source!.read }), ...(document._source!.state && { state: document._source!.state }), }; } @@ -209,6 +212,7 @@ export const fromEs = (document: Document): Conversation => { return { ...base, rounds: roundsWithRefs, + ...(document._source!.read !== undefined && { read: document._source!.read }), ...(document._source!.state && { state: document._source!.state }), }; }; @@ -231,6 +235,7 @@ export const toEs = (conversation: Conversation, space: string): ConversationPro conversation_rounds: serializeStepResults(conversation.rounds), attachments: conversation.attachments ?? [], state: conversation.state, + ...(conversation.read !== undefined && { read: conversation.read }), }; }; @@ -277,5 +282,6 @@ export const createRequestToEs = ({ conversation_rounds: serializeStepResults(conversation.rounds), attachments: conversation.attachments ?? [], state: conversation.state, + ...(conversation.read !== undefined && { read: conversation.read }), }; }; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts index 5ff7107f0a66a..9686b3f781d0d 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts @@ -29,6 +29,7 @@ const storageSettings = { conversation_rounds: types.object({ dynamic: false, properties: {} }), attachments: types.object({ dynamic: false, properties: {} }), state: types.object({ dynamic: false, properties: {} }), + read: types.boolean({}), }, }, } satisfies IndexStorageSettings; @@ -44,6 +45,7 @@ export interface ConversationProperties { conversation_rounds: PersistentConversationRound[]; attachments?: VersionedAttachment[]; state?: ConversationInternalState; + read?: boolean; // legacy field rounds?: PersistentConversationRound[]; } diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts index 0117aa41f89cb..0030f6c317eb1 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts @@ -27,7 +27,7 @@ export type ConversationCreateRequest = Omit< }; export type ConversationUpdateRequest = Pick & - Partial>; + Partial>; export interface ConversationListOptions { agentId?: string; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts index 3271bb5b0ca85..66668d8a50f05 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts @@ -42,6 +42,7 @@ export const createConversation$ = ({ title, agent_id: agentId, state: roundCompletedEvent.data.conversation_state, + read: false, rounds: [roundCompletedEvent.data.round], ...(roundCompletedEvent.data.attachments ? { attachments: roundCompletedEvent.data.attachments } @@ -87,6 +88,7 @@ export const updateConversation$ = ({ title, rounds: updatedRound, state: conversation_state, + read: false, ...(roundCompletedEvent.data.attachments !== undefined ? { attachments: roundCompletedEvent.data.attachments } : {}), From 49bf79d7b24495c12cdb65663ef1d9a47d49c2da Mon Sep 17 00:00:00 2001 From: Petr Krejcik Date: Thu, 28 May 2026 09:46:15 +0200 Subject: [PATCH 2/7] add `status` --- .../agent-builder-common/chat/conversation.ts | 2 ++ .../server/services/conversation/client/client.ts | 11 ++++++++++- .../services/conversation/client/converters.ts | 13 +++++++------ .../server/services/conversation/client/storage.ts | 7 ++++++- .../server/services/conversation/client/types.ts | 2 +- .../services/execution/utils/conversations.ts | 10 ++++++---- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts b/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts index 2a4e73022820d..b8995269f9c0d 100644 --- a/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts +++ b/x-pack/platform/packages/shared/agent-builder/agent-builder-common/chat/conversation.ts @@ -350,6 +350,8 @@ export interface Conversation { * Any new or updated conversation has `read` set to `false` by default */ read?: boolean; + /** current status of the conversation */ + status?: ConversationRoundStatus; } export type TodoStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts index eecc49d2bc265..1b43048cc7405 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts @@ -79,7 +79,16 @@ class ConversationClientImpl implements ConversationClient { const response = await this.storage.getClient().search({ track_total_hits: false, size: 1000, - _source: ['agent_id', 'user_id', 'user_name', 'title', 'created_at', 'updated_at', 'read'], + _source: [ + 'agent_id', + 'user_id', + 'user_name', + 'title', + 'created_at', + 'updated_at', + 'read', + 'status', + ], query: { bool: { filter: [createSpaceDslFilter(this.space)], diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts index 34a9f4e7bfb25..2a3d6c296a227 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts @@ -60,6 +60,7 @@ const convertBaseFromEs = (document: Document) => { created_at: document._source.created_at, updated_at: document._source.updated_at, ...(document._source.read !== undefined && { read: document._source.read }), + ...(document._source.status !== undefined && { status: document._source.status }), }; }; @@ -194,8 +195,6 @@ export const fromEs = (document: Document): Conversation => { ...base, rounds: roundsWithRefs, attachments: existingAttachments, - ...(document._source!.read !== undefined && { read: document._source!.read }), - ...(document._source!.state && { state: document._source!.state }), }; } @@ -204,16 +203,12 @@ export const fromEs = (document: Document): Conversation => { ...base, rounds: roundsWithRefs, ...(attachmentsForRefs.length > 0 && { attachments: attachmentsForRefs }), - ...(document._source!.read !== undefined && { read: document._source!.read }), - ...(document._source!.state && { state: document._source!.state }), }; } return { ...base, rounds: roundsWithRefs, - ...(document._source!.read !== undefined && { read: document._source!.read }), - ...(document._source!.state && { state: document._source!.state }), }; }; @@ -235,6 +230,9 @@ export const toEs = (conversation: Conversation, space: string): ConversationPro conversation_rounds: serializeStepResults(conversation.rounds), attachments: conversation.attachments ?? [], state: conversation.state, + ...(conversation.rounds.length > 0 && { + status: conversation.rounds[conversation.rounds.length - 1].status, + }), ...(conversation.read !== undefined && { read: conversation.read }), }; }; @@ -282,6 +280,9 @@ export const createRequestToEs = ({ conversation_rounds: serializeStepResults(conversation.rounds), attachments: conversation.attachments ?? [], state: conversation.state, + ...(conversation.rounds.length > 0 && { + status: conversation.rounds[conversation.rounds.length - 1].status, + }), ...(conversation.read !== undefined && { read: conversation.read }), }; }; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts index 9686b3f781d0d..e4558768bd7b8 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/storage.ts @@ -10,7 +10,10 @@ import type { IndexStorageSettings } from '@kbn/storage-adapter'; import { StorageIndexAdapter, types } from '@kbn/storage-adapter'; import { chatSystemIndex } from '@kbn/agent-builder-server'; import type { VersionedAttachment } from '@kbn/agent-builder-common/attachments'; -import type { ConversationInternalState } from '@kbn/agent-builder-common/chat'; +import type { + ConversationInternalState, + ConversationRoundStatus, +} from '@kbn/agent-builder-common/chat'; import type { PersistentConversationRound } from './types'; export const conversationIndexName = chatSystemIndex('conversations'); @@ -29,6 +32,7 @@ const storageSettings = { conversation_rounds: types.object({ dynamic: false, properties: {} }), attachments: types.object({ dynamic: false, properties: {} }), state: types.object({ dynamic: false, properties: {} }), + status: types.keyword({}), read: types.boolean({}), }, }, @@ -45,6 +49,7 @@ export interface ConversationProperties { conversation_rounds: PersistentConversationRound[]; attachments?: VersionedAttachment[]; state?: ConversationInternalState; + status?: ConversationRoundStatus; read?: boolean; // legacy field rounds?: PersistentConversationRound[]; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts index 0030f6c317eb1..dad872ff32320 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts @@ -27,7 +27,7 @@ export type ConversationCreateRequest = Omit< }; export type ConversationUpdateRequest = Pick & - Partial>; + Partial>; export interface ConversationListOptions { agentId?: string; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts index 66668d8a50f05..4c04e1edaa851 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts @@ -8,10 +8,10 @@ import { v4 as uuidv4 } from 'uuid'; import type { Observable } from 'rxjs'; import { of, forkJoin, switchMap } from 'rxjs'; -import type { - Conversation, - RoundCompleteEvent, - ConversationAction, +import { + type Conversation, + type RoundCompleteEvent, + type ConversationAction, } from '@kbn/agent-builder-common'; import type { ConversationClient } from '../../conversation'; import { createConversationUpdatedEvent, createConversationCreatedEvent } from './events'; @@ -42,6 +42,7 @@ export const createConversation$ = ({ title, agent_id: agentId, state: roundCompletedEvent.data.conversation_state, + status: roundCompletedEvent.data.round.status, read: false, rounds: [roundCompletedEvent.data.round], ...(roundCompletedEvent.data.attachments @@ -89,6 +90,7 @@ export const updateConversation$ = ({ rounds: updatedRound, state: conversation_state, read: false, + status: round.status, ...(roundCompletedEvent.data.attachments !== undefined ? { attachments: roundCompletedEvent.data.attachments } : {}), From 7606cdcd9f0da2e2f106a0b844c96c8712261d06 Mon Sep 17 00:00:00 2001 From: Petr Krejcik Date: Thu, 28 May 2026 10:40:52 +0200 Subject: [PATCH 3/7] lint --- .../server/routes/internal/conversations.ts | 9 +++++---- .../server/services/conversation/client/client.ts | 2 +- .../server/services/conversation/client/converters.ts | 3 +++ .../server/services/execution/utils/conversations.ts | 10 +++++----- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts index 08a244cd2a1ed..ea700a06261bd 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts @@ -6,6 +6,7 @@ */ import { schema } from '@kbn/config-schema'; +import { createInternalError } from '@kbn/agent-builder-common'; import type { RouteDependencies } from '../types'; import { getHandlerWrapper } from '../wrap_handler'; import type { @@ -86,11 +87,11 @@ export function registerInternalConversationRoutes({ read, }); - // Do this validation in order to be able to return `read` that is always defined instead of an optional `read` which is in the Conversation type. + // Enables `read` to be mandatory in the response and not `read?`. if (read !== updatedConversation.read) { - // TODO: check what error to throw, there are also x-pack/platform/packages/shared/agent-builder/agent-builder-common/base/errors.ts - throw new Error( - `Failed to persist read state for conversation ${conversationId}: expected ${read}, got ${updatedConversation.read}` + throw createInternalError( + `Failed to persist read state for conversation ${conversationId}: expected ${read}, got ${updatedConversation.read}`, + { conversationId, expected: read, actual: updatedConversation.read } ); } diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts index 1b43048cc7405..1e028594cdddc 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/client.ts @@ -86,8 +86,8 @@ class ConversationClientImpl implements ConversationClient { 'title', 'created_at', 'updated_at', - 'read', 'status', + 'read', ], query: { bool: { diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts index 2a3d6c296a227..d9544e1e94be9 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts @@ -195,6 +195,7 @@ export const fromEs = (document: Document): Conversation => { ...base, rounds: roundsWithRefs, attachments: existingAttachments, + ...(document._source!.state && { state: document._source!.state }), }; } @@ -203,12 +204,14 @@ export const fromEs = (document: Document): Conversation => { ...base, rounds: roundsWithRefs, ...(attachmentsForRefs.length > 0 && { attachments: attachmentsForRefs }), + ...(document._source!.state && { state: document._source!.state }), }; } return { ...base, rounds: roundsWithRefs, + ...(document._source!.state && { state: document._source!.state }), }; }; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts index 4c04e1edaa851..af5a886dbc834 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.ts @@ -8,10 +8,10 @@ import { v4 as uuidv4 } from 'uuid'; import type { Observable } from 'rxjs'; import { of, forkJoin, switchMap } from 'rxjs'; -import { - type Conversation, - type RoundCompleteEvent, - type ConversationAction, +import type { + Conversation, + RoundCompleteEvent, + ConversationAction, } from '@kbn/agent-builder-common'; import type { ConversationClient } from '../../conversation'; import { createConversationUpdatedEvent, createConversationCreatedEvent } from './events'; @@ -89,8 +89,8 @@ export const updateConversation$ = ({ title, rounds: updatedRound, state: conversation_state, - read: false, status: round.status, + read: false, ...(roundCompletedEvent.data.attachments !== undefined ? { attachments: roundCompletedEvent.data.attachments } : {}), From 9cb48965c3ca26b67d32040fc9a31331c352e572 Mon Sep 17 00:00:00 2001 From: Petr Krejcik Date: Thu, 28 May 2026 10:43:04 +0200 Subject: [PATCH 4/7] lint --- .../server/services/conversation/client/converters.ts | 2 +- .../agent_builder/server/services/conversation/client/types.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts index d9544e1e94be9..6ae66f816d28f 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts @@ -59,8 +59,8 @@ const convertBaseFromEs = (document: Document) => { title: document._source.title, created_at: document._source.created_at, updated_at: document._source.updated_at, - ...(document._source.read !== undefined && { read: document._source.read }), ...(document._source.status !== undefined && { status: document._source.status }), + ...(document._source.read !== undefined && { read: document._source.read }), }; }; diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts index dad872ff32320..7279b2c69fed4 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/types.ts @@ -27,7 +27,7 @@ export type ConversationCreateRequest = Omit< }; export type ConversationUpdateRequest = Pick & - Partial>; + Partial>; export interface ConversationListOptions { agentId?: string; From 30d8c476e79bb6987d9cda3279a8414187fda2c0 Mon Sep 17 00:00:00 2001 From: Petr Krejcik Date: Fri, 29 May 2026 07:18:07 +0200 Subject: [PATCH 5/7] always store `read:false` on creation --- .../server/services/conversation/client/converters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts index 6ae66f816d28f..5ffa22c7db5b7 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts @@ -286,6 +286,6 @@ export const createRequestToEs = ({ ...(conversation.rounds.length > 0 && { status: conversation.rounds[conversation.rounds.length - 1].status, }), - ...(conversation.read !== undefined && { read: conversation.read }), + read: false, }; }; From ed6efd539762de8bd422669d5c3280ae5f904aa5 Mon Sep 17 00:00:00 2001 From: Petr Krejcik Date: Fri, 29 May 2026 07:40:10 +0200 Subject: [PATCH 6/7] remove conditionals --- .../services/conversation/client/converters.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts index 5ffa22c7db5b7..625d5f1e2a784 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/conversation/client/converters.ts @@ -59,8 +59,8 @@ const convertBaseFromEs = (document: Document) => { title: document._source.title, created_at: document._source.created_at, updated_at: document._source.updated_at, - ...(document._source.status !== undefined && { status: document._source.status }), - ...(document._source.read !== undefined && { read: document._source.read }), + status: document._source.status, + read: document._source.read, }; }; @@ -233,10 +233,8 @@ export const toEs = (conversation: Conversation, space: string): ConversationPro conversation_rounds: serializeStepResults(conversation.rounds), attachments: conversation.attachments ?? [], state: conversation.state, - ...(conversation.rounds.length > 0 && { - status: conversation.rounds[conversation.rounds.length - 1].status, - }), - ...(conversation.read !== undefined && { read: conversation.read }), + status: conversation.status, + read: conversation.read, }; }; @@ -283,9 +281,7 @@ export const createRequestToEs = ({ conversation_rounds: serializeStepResults(conversation.rounds), attachments: conversation.attachments ?? [], state: conversation.state, - ...(conversation.rounds.length > 0 && { - status: conversation.rounds[conversation.rounds.length - 1].status, - }), + status: conversation.status, read: false, }; }; From daa5aa34d3f8251eaf474bd2942129e825b17158 Mon Sep 17 00:00:00 2001 From: Petr Krejcik Date: Fri, 29 May 2026 10:06:14 +0200 Subject: [PATCH 7/7] tests --- .../routes/internal/conversations.test.ts | 93 +++++++++++++++++++ .../server/routes/internal/conversations.ts | 2 +- .../execution/utils/conversations.test.ts | 6 ++ 3 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.test.ts diff --git a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.test.ts b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.test.ts new file mode 100644 index 0000000000000..fb330b674fcd3 --- /dev/null +++ b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IRouter } from '@kbn/core/server'; +import { kibanaResponseFactory } from '@kbn/core/server'; +import { httpServerMock, loggingSystemMock } from '@kbn/core/server/mocks'; +import { registerInternalConversationRoutes } from './conversations'; +import type { RouteDependencies } from '../types'; +import { internalApiPath } from '../../../common/constants'; + +const MARK_READ_PATH = `${internalApiPath}/conversations/{conversation_id}/_mark_read`; + +describe('registerInternalConversationRoutes - _mark_read', () => { + let routeHandler: (ctx: any, req: any, res: any) => Promise; + let update: jest.Mock; + + const createMockContext = () => ({ + core: Promise.resolve({}), + licensing: Promise.resolve({ + license: { status: 'active', hasAtLeast: jest.fn().mockReturnValue(true) }, + }), + }); + + const createRequest = (overrides: { params?: object; body?: object } = {}) => + httpServerMock.createKibanaRequest({ + method: 'post', + path: MARK_READ_PATH, + params: { conversation_id: 'conv-1' }, + body: { read: true }, + ...overrides, + }); + + beforeEach(() => { + jest.clearAllMocks(); + + update = jest.fn().mockResolvedValue({ id: 'conv-1', read: true }); + + const getInternalServices = jest.fn().mockReturnValue({ + conversations: { + getScopedClient: jest.fn().mockResolvedValue({ update }), + }, + }); + + const routeHandlers: Record Promise> = {}; + + const router = { + post: jest + .fn() + .mockImplementation( + (config: { path: string }, handler: (ctx: any, req: any, res: any) => Promise) => { + routeHandlers[config.path] = handler; + } + ), + } as unknown as IRouter; + + registerInternalConversationRoutes({ + router, + getInternalServices, + logger: loggingSystemMock.createLogger(), + } as unknown as RouteDependencies); + + routeHandler = routeHandlers[MARK_READ_PATH]; + }); + + it('calls client.update and returns id and read on success', async () => { + const response = await routeHandler( + createMockContext() as any, + createRequest(), + kibanaResponseFactory + ); + + expect(update).toHaveBeenCalledWith({ id: 'conv-1', read: true }); + expect(response.status).toBe(200); + expect(response.payload).toMatchObject({ id: 'conv-1', read: true }); + }); + + it('returns 500 when the persisted read value does not match the requested value', async () => { + // Simulates ES returning a stale/legacy doc where `read` is undefined + update.mockResolvedValue({ id: 'conv-1', read: undefined }); + + const response = await routeHandler( + createMockContext() as any, + createRequest(), + kibanaResponseFactory + ); + + expect(response.status).toBe(500); + }); +}); diff --git a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts index ea700a06261bd..f227184b56c7d 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/routes/internal/conversations.ts @@ -65,7 +65,7 @@ export function registerInternalConversationRoutes({ path: `${internalApiPath}/conversations/{conversation_id}/_mark_read`, validate: { params: schema.object({ - conversation_id: schema.string(), + conversation_id: schema.string({ maxLength: 256 }), }), body: schema.object({ read: schema.boolean(), diff --git a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.test.ts b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.test.ts index a4d57d841c81e..dbb5a03c908f1 100644 --- a/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.test.ts +++ b/x-pack/platform/plugins/shared/agent_builder/server/services/execution/utils/conversations.test.ts @@ -111,6 +111,8 @@ describe('conversations utils', () => { expect(conversationClient.update).toHaveBeenCalledWith( expect.objectContaining({ rounds: [newRound], + read: false, + status: newRound.status, }) ); }); @@ -147,6 +149,8 @@ describe('conversations utils', () => { expect(conversationClient.update).toHaveBeenCalledWith( expect.objectContaining({ rounds: [existingRound, newRound], + read: false, + status: newRound.status, }) ); }); @@ -184,6 +188,8 @@ describe('conversations utils', () => { expect(conversationClient.update).toHaveBeenCalledWith( expect.objectContaining({ rounds: [newRound], + read: false, + status: newRound.status, }) ); });