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
9 changes: 7 additions & 2 deletions packages/client-react/src/stores/session-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export const useSessionStore = create<SessionState>((set, get) => ({
},

sendMessage: async (content) => {
const { sessionId, entryAgentId, pendingQuestions, pendingAttachments } = get()
const { sessionId, entryAgentId, pendingQuestions, pendingAttachments, messageContext } = get()
if (!sessionId || !entryAgentId || pendingQuestions.length > 0) return

// Collect ready attachments for display
Expand Down Expand Up @@ -252,7 +252,12 @@ export const useSessionStore = create<SessionState>((set, get) => ({
try {
// Use content or a placeholder if only attachments
const messageContent = content.trim() || (readyAttachments.length > 0 ? `[${readyAttachments.map((a) => a.filename).join(', ')}]` : '')
unwrap(await api.call('user-chat.sendMessage', { sessionId, agentId: AgentId(entryAgentId), content: messageContent }))
unwrap(await api.call('user-chat.sendMessage', {
sessionId,
agentId: AgentId(entryAgentId),
content: messageContent,
context: messageContext ?? undefined,
}))
// Remove from pending on success and clear attachments
const newPendingMessages = new Map(get().pendingMessages)
newPendingMessages.delete(messageId)
Expand Down
15 changes: 13 additions & 2 deletions packages/sdk/src/plugins/user-chat/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const userChatEvents = createEventsFactory({
agentId: agentIdSchema,
messageId: chatMessageIdSchema,
content: z.string(),
context: z.string().optional(),
timestamp: z.number(),
}),
user_chat_answer_received: z.object({
Expand Down Expand Up @@ -95,6 +96,8 @@ export interface UserChatMessage {
type: "user_message";
messageId: ChatMessageId;
content: string;
/** What the client knew when the user sent the message; the LLM sees it, the chat does not. */
context?: string;
timestamp: number;
}

Expand Down Expand Up @@ -384,6 +387,9 @@ function formatPendingForLLM(
? indexedMessage
: findUserMessage(messages, msg.messageId);
if (userMessage) parts.push(`[User]: ${userMessage.content}`);
if (userMessage?.context) {
parts.push(`<user_context>\n${userMessage.context}\n</user_context>`);
}
}
}
return parts.join("\n");
Expand Down Expand Up @@ -469,6 +475,7 @@ export const userChatPlugin = definePlugin("user-chat")
type: "user_message",
messageId: event.messageId,
content: event.content,
...(event.context !== undefined && { context: event.context }),
timestamp: event.timestamp,
};
const pending: PendingInboundMessage = {
Expand Down Expand Up @@ -676,6 +683,7 @@ export const userChatPlugin = definePlugin("user-chat")
input: z.object({
agentId: agentIdSchema.optional(),
content: z.string(),
context: z.string().optional(),
}),
output: z.object({
messageId: chatMessageIdSchema,
Expand All @@ -688,10 +696,12 @@ export const userChatPlugin = definePlugin("user-chat")

// Hard limit
const tokenCount = estimateTokens(input.content);
if (tokenCount > MESSAGE_MAX_TOKENS) {
const totalTokenCount =
tokenCount + (input.context === undefined ? 0 : estimateTokens(input.context));
if (totalTokenCount > MESSAGE_MAX_TOKENS) {
return Err(
ValidationErrors.invalid(
`Message too large: ~${tokenCount} tokens (max ${MESSAGE_MAX_TOKENS})`,
`Message too large: ~${totalTokenCount} tokens (max ${MESSAGE_MAX_TOKENS})`,
),
);
}
Expand Down Expand Up @@ -728,6 +738,7 @@ export const userChatPlugin = definePlugin("user-chat")
agentId: agentId,
messageId,
content,
...(input.context !== undefined && { context: input.context }),
timestamp: Date.now(),
}),
);
Expand Down
47 changes: 47 additions & 0 deletions packages/sdk/src/plugins/user-chat/user-chat.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,53 @@ describe('user-chat plugin', () => {
await harness.shutdown()
})

it('sendMessage with context → LLM sees the context, chat history keeps it apart from content', async () => {
const harness = new TestHarness({
presets: [createTestPreset()],
llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }),
})

const session = await harness.createSession('test')
const result = await session.callPluginMethod('user-chat.sendMessage', {
content: 'Make this heading bigger',
context: 'Page: /about',
})
expect(result.ok).toBe(true)
await session.waitForIdle()

const lastRequest = harness.llmProvider.getLastRequest()
const lastMessage = lastRequest!.messages[lastRequest!.messages.length - 1]
expect(lastMessage.content).toBe('[User]: Make this heading bigger\n<user_context>\nPage: /about\n</user_context>')

const state = selectPluginState<UserChatState>(session.state, 'messages')
expect(state?.messages).toContainEqual(expect.objectContaining({
type: 'user_message',
content: 'Make this heading bigger',
context: 'Page: /about',
}))

await harness.shutdown()
})

it('sendMessage without context → LLM gets no context block', async () => {
const harness = new TestHarness({
presets: [createTestPreset()],
llmProvider: MockLLMProvider.withFixedResponse({ content: 'Ok', toolCalls: [] }),
})

const session = await harness.createSession('test')
await session.sendAndWaitForIdle('Hello world')

const lastRequest = harness.llmProvider.getLastRequest()
const lastMessage = lastRequest!.messages[lastRequest!.messages.length - 1]
expect(lastMessage.content).toBe('[User]: Hello world')

const events = await session.getEventsByType('user_chat_message_received')
expect(events[0]).not.toHaveProperty('context')

await harness.shutdown()
})

it('sendMessage to specific agent (non-entry) via sendMessageToAgent', async () => {
const harness = new TestHarness({
presets: [createTestPreset()],
Expand Down
Loading