Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/supporting-amaranth-felidae.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/agents-api": patch
---

Fix mid-conversation userProperties/properties not persisting and active sub-agent being reset across turns in chat routes
26 changes: 13 additions & 13 deletions agents-api/src/__tests__/run/routes/chat/userProperties.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ import { makeRequest } from '../../../utils/testRequest';

describe('userProperties in chat requests', () => {
it('writes body.userProperties to top-level conversations.userProperties via /chat (Vercel stream)', async () => {
mockSetActiveAgent.mockClear();
mockCreateOrGetConversation.mockClear();
mockCreateMessage.mockClear();

const body = {
Expand All @@ -118,7 +118,7 @@ describe('userProperties in chat requests', () => {
});

expect(res.status).toBe(200);
expect(mockSetActiveAgent).toHaveBeenCalledWith(
expect(mockCreateOrGetConversation).toHaveBeenCalledWith(
expect.objectContaining({
userProperties: { email: 'test@example.com', plan: 'pro' },
})
Expand All @@ -143,7 +143,7 @@ describe('userProperties in chat requests', () => {
});

it('writes body.userProperties + body.properties to top-level columns via /chat (Vercel stream)', async () => {
mockSetActiveAgent.mockClear();
mockCreateOrGetConversation.mockClear();

const body = {
messages: [{ role: 'user', content: 'Hello' }],
Expand All @@ -157,7 +157,7 @@ describe('userProperties in chat requests', () => {
});

expect(res.status).toBe(200);
expect(mockSetActiveAgent).toHaveBeenCalledWith(
expect(mockCreateOrGetConversation).toHaveBeenCalledWith(
expect.objectContaining({
userProperties: { email: 'stream@example.com', plan: 'pro' },
properties: { url: '/stream-page', referrer: 'duck' },
Expand Down Expand Up @@ -237,7 +237,7 @@ describe('userProperties in chat requests', () => {
});

it('does not set userProperties or metadata when both are omitted', async () => {
mockSetActiveAgent.mockClear();
mockCreateOrGetConversation.mockClear();
mockCreateMessage.mockClear();

const body = {
Expand All @@ -250,7 +250,7 @@ describe('userProperties in chat requests', () => {
});

expect(res.status).toBe(200);
expect(mockSetActiveAgent).toHaveBeenCalledWith(
expect(mockCreateOrGetConversation).toHaveBeenCalledWith(
expect.not.objectContaining({ userProperties: expect.anything() })
);
expect(mockCreateMessage).toHaveBeenCalledWith(
Expand All @@ -264,7 +264,7 @@ describe('userProperties in chat requests', () => {
['ANONYMOUS', { id: '1hb1l6c4cg9435m125i6p', identificationType: 'ANONYMOUS' }],
['COOKIED', { id: '1hb1l6c4cg9435m125i6p', identificationType: 'COOKIED' }],
])('drops widget auto-mint identityType=%s userProperties', async (_label, userProperties) => {
mockSetActiveAgent.mockClear();
mockCreateOrGetConversation.mockClear();
mockCreateMessage.mockClear();

const body = {
Expand All @@ -278,7 +278,7 @@ describe('userProperties in chat requests', () => {
});

expect(res.status).toBe(200);
expect(mockSetActiveAgent).toHaveBeenCalledWith(
expect(mockCreateOrGetConversation).toHaveBeenCalledWith(
expect.not.objectContaining({ userProperties: expect.anything() })
);
expect(mockCreateMessage).toHaveBeenCalledWith(
Expand All @@ -289,7 +289,7 @@ describe('userProperties in chat requests', () => {
});

it('drops ANONYMOUS userProperties supplied via x-inkeep-user-properties header', async () => {
mockSetActiveAgent.mockClear();
mockCreateOrGetConversation.mockClear();
mockCreateMessage.mockClear();

const body = { messages: [{ role: 'user', content: 'Hello' }] };
Expand All @@ -306,13 +306,13 @@ describe('userProperties in chat requests', () => {
});

expect(res.status).toBe(200);
expect(mockSetActiveAgent).toHaveBeenCalledWith(
expect(mockCreateOrGetConversation).toHaveBeenCalledWith(
expect.not.objectContaining({ userProperties: expect.anything() })
);
});

it('preserves ID_PROVIDED userProperties (header) and strips identificationType marker', async () => {
mockSetActiveAgent.mockClear();
mockCreateOrGetConversation.mockClear();
mockCreateMessage.mockClear();

const body = { messages: [{ role: 'user', content: 'Hello' }] };
Expand All @@ -331,12 +331,12 @@ describe('userProperties in chat requests', () => {
});

expect(res.status).toBe(200);
expect(mockSetActiveAgent).toHaveBeenCalledWith(
expect(mockCreateOrGetConversation).toHaveBeenCalledWith(
expect.objectContaining({
userProperties: { id: 'customer-42', email: 'customer@example.com' },
})
);
expect(mockSetActiveAgent).toHaveBeenCalledWith(
expect(mockCreateOrGetConversation).toHaveBeenCalledWith(
expect.objectContaining({
userProperties: expect.not.objectContaining({ identificationType: expect.anything() }),
})
Expand Down
2 changes: 1 addition & 1 deletion agents-api/src/domains/run/routes/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ app.openapi(chatCompletionsRoute, async (c) => {
projectId,
id: conversationId,
agentId: agentId,
activeSubAgentId: defaultSubAgentId,
activeSubAgentId: existingActiveAgent?.activeSubAgentId ?? defaultSubAgentId,
ref: executionContext.resolvedRef,
userId: executionContext.metadata?.endUserId,
...(conversationMeta ? { metadata: conversationMeta } : {}),
Expand Down
19 changes: 14 additions & 5 deletions agents-api/src/domains/run/routes/chatDataStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
commonGetErrorResponses,
createApiError,
createMessage,
createOrGetConversation,
type FullExecutionContext,
generateId,
getActiveAgentForConversation,
Expand Down Expand Up @@ -353,8 +354,20 @@ app.openapi(chatDataStreamRoute, async (c) => {
headerName: 'x-inkeep-properties',
logger,
});
const conversationMeta = buildConversationMetadata(executionContext);
await createOrGetConversation(runDbClient)({
tenantId,
projectId,
id: conversationId,
agentId: agentId,
activeSubAgentId: activeAgent?.activeSubAgentId ?? defaultSubAgentId,
ref: executionContext.resolvedRef,
userId: executionContext.metadata?.endUserId,
...(conversationMeta ? { metadata: conversationMeta } : {}),
...(resolvedUserProperties !== undefined ? { userProperties: resolvedUserProperties } : {}),
...(resolvedProperties !== undefined ? { properties: resolvedProperties } : {}),
});
if (!activeAgent) {
const conversationMeta = buildConversationMetadata(executionContext);
await setActiveAgentForConversation(runDbClient)({
scopes: { tenantId, projectId },
conversationId,
Expand All @@ -363,10 +376,6 @@ app.openapi(chatDataStreamRoute, async (c) => {
agentId: agentId,
userId: executionContext.metadata?.endUserId,
...(conversationMeta ? { metadata: conversationMeta } : {}),
...(resolvedUserProperties !== undefined
? { userProperties: resolvedUserProperties }
: {}),
...(resolvedProperties !== undefined ? { properties: resolvedProperties } : {}),
});
}
const subAgentId = activeAgent?.activeSubAgentId || defaultSubAgentId;
Expand Down
Loading