Compaction overhaul (#5186)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: David Katz <dkatz@squareup.com>
Co-authored-by: Alex Hancock <alexhancock@block.xyz>
This commit is contained in:
Douwe Osinga
2025-10-15 18:39:12 -04:00
committed by GitHub
parent ef9e68f2c6
commit 7c1f5dcb2f
56 changed files with 3069 additions and 4465 deletions
+20 -51
View File
@@ -2251,30 +2251,14 @@
}
]
},
"ContextLengthExceeded": {
"type": "object",
"required": [
"msg"
],
"properties": {
"msg": {
"type": "string"
}
}
},
"ContextManageRequest": {
"type": "object",
"description": "Request payload for context management operations",
"required": [
"messages",
"manageAction",
"sessionId"
],
"properties": {
"manageAction": {
"type": "string",
"description": "Operation to perform: \"truncation\" or \"summarize\""
},
"messages": {
"type": "array",
"items": {
@@ -2319,6 +2303,17 @@
"$ref": "#/components/schemas/Message"
}
},
"ConversationCompacted": {
"type": "object",
"required": [
"msg"
],
"properties": {
"msg": {
"type": "string"
}
}
},
"CreateRecipeRequest": {
"type": "object",
"required": [
@@ -3136,7 +3131,9 @@
"description": "A message to or from an LLM",
"required": [
"role",
"content"
"created",
"content",
"metadata"
],
"properties": {
"content": {
@@ -3334,7 +3331,7 @@
{
"allOf": [
{
"$ref": "#/components/schemas/ContextLengthExceeded"
"$ref": "#/components/schemas/ConversationCompacted"
},
{
"type": "object",
@@ -3345,28 +3342,7 @@
"type": {
"type": "string",
"enum": [
"contextLengthExceeded"
]
}
}
}
]
},
{
"allOf": [
{
"$ref": "#/components/schemas/SummarizationRequested"
},
{
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"summarizationRequested"
"conversationCompacted"
]
}
}
@@ -3382,6 +3358,10 @@
"MessageMetadata": {
"type": "object",
"description": "Metadata for message visibility",
"required": [
"userVisible",
"agentVisible"
],
"properties": {
"agentVisible": {
"type": "boolean",
@@ -4429,17 +4409,6 @@
"propertyName": "type"
}
},
"SummarizationRequested": {
"type": "object",
"required": [
"msg"
],
"properties": {
"msg": {
"type": "string"
}
}
},
"TextContent": {
"type": "object",
"required": [
+10 -20
View File
@@ -72,18 +72,10 @@ export type ConfigResponse = {
export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | RawAudioContent | RawResource;
export type ContextLengthExceeded = {
msg: string;
};
/**
* Request payload for context management operations
*/
export type ContextManageRequest = {
/**
* Operation to perform: "truncation" or "summarize"
*/
manageAction: string;
/**
* Collection of messages to be managed
*/
@@ -110,6 +102,10 @@ export type ContextManageResponse = {
export type Conversation = Array<Message>;
export type ConversationCompacted = {
msg: string;
};
export type CreateRecipeRequest = {
author?: AuthorRequest | null;
session_id: string;
@@ -383,9 +379,9 @@ export type LoadedProvider = {
*/
export type Message = {
content: Array<MessageContent>;
created?: number;
created: number;
id?: string | null;
metadata?: MessageMetadata;
metadata: MessageMetadata;
role: Role;
};
@@ -408,10 +404,8 @@ export type MessageContent = (TextContent & {
type: 'thinking';
}) | (RedactedThinkingContent & {
type: 'redactedThinking';
}) | (ContextLengthExceeded & {
type: 'contextLengthExceeded';
}) | (SummarizationRequested & {
type: 'summarizationRequested';
}) | (ConversationCompacted & {
type: 'conversationCompacted';
});
/**
@@ -421,11 +415,11 @@ export type MessageMetadata = {
/**
* Whether the message should be included in the agent's context window
*/
agentVisible?: boolean;
agentVisible: boolean;
/**
* Whether the message should be visible to the user in the UI
*/
userVisible?: boolean;
userVisible: boolean;
};
/**
@@ -794,10 +788,6 @@ export type SuccessCheck = {
type: 'Shell';
};
export type SummarizationRequested = {
msg: string;
};
export type TextContent = {
_meta?: {
[key: string]: unknown;
@@ -172,7 +172,9 @@ export default function ProgressiveMessageList({
const messagesToRender = messages.slice(0, renderedCount);
return messagesToRender
.map((message, index) => {
// Use custom render function if provided
if (!message.metadata.userVisible) {
return null;
}
if (renderMessage) {
return renderMessage(message, index);
}
@@ -1,5 +1,5 @@
import React from 'react';
import { Message, SummarizationRequested } from '../../api';
import { Message, ConversationCompacted } from '../../api';
interface CompactionMarkerProps {
message: Message;
@@ -7,8 +7,8 @@ interface CompactionMarkerProps {
export const CompactionMarker: React.FC<CompactionMarkerProps> = ({ message }) => {
const compactionContent = message.content.find(
(content): content is SummarizationRequested & { type: 'summarizationRequested' } =>
content.type === 'summarizationRequested'
(content): content is ConversationCompacted & { type: 'conversationCompacted' } =>
content.type === 'conversationCompacted'
);
const markerText = compactionContent?.msg || 'Conversation compacted';
@@ -9,12 +9,6 @@ interface ContextManagerState {
}
interface ContextManagerActions {
handleAutoCompaction: (
messages: Message[],
setMessages: (messages: Message[]) => void,
append: (message: Message) => void,
sessionId: string
) => Promise<void>;
handleManualCompaction: (
messages: Message[],
setMessages: (messages: Message[]) => void,
@@ -70,6 +64,7 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
setIsCompacting(false);
} catch (err) {
// TODO(Douwe): move this to the server
console.error('Error during compaction:', err);
setCompactionError(err instanceof Error ? err.message : 'Unknown error during compaction');
@@ -80,10 +75,11 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
created: Math.floor(Date.now() / 1000),
content: [
{
type: 'summarizationRequested',
type: 'conversationCompacted',
msg: 'Compaction failed. Please try again or start a new session.',
},
],
metadata: { userVisible: true, agentVisible: true },
};
setMessages([...messages, errorMarker]);
@@ -93,18 +89,6 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
[]
);
const handleAutoCompaction = useCallback(
async (
messages: Message[],
setMessages: (messages: Message[]) => void,
append: (message: Message) => void,
sessionId: string
) => {
await performCompaction(messages, setMessages, append, sessionId, false);
},
[performCompaction]
);
const handleManualCompaction = useCallback(
async (
messages: Message[],
@@ -118,7 +102,7 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
);
const hasCompactionMarker = useCallback((message: Message): boolean => {
return message.content.some((content) => content.type === 'summarizationRequested');
return message.content.some((content) => content.type === 'conversationCompacted');
}, []);
const value = {
@@ -127,7 +111,6 @@ export const ContextManagerProvider: React.FC<{ children: React.ReactNode }> = (
compactionError,
// Actions
handleAutoCompaction,
handleManualCompaction,
hasCompactionMarker,
};
@@ -3,12 +3,20 @@ import { render, screen } from '@testing-library/react';
import { CompactionMarker } from '../CompactionMarker';
import { Message } from '../../../api';
const default_message: Message = {
metadata: {
agentVisible: false,
userVisible: false,
},
id: '1',
role: 'assistant',
created: 1000,content: []
};
describe('CompactionMarker', () => {
it('should render default message when no summarizationRequested content found', () => {
it('should render default message when no conversationCompacted content found', () => {
const message: Message = {
id: '1',
role: 'assistant',
created: 1000,
...default_message,
content: [{ type: 'text', text: 'Regular message' }],
};
@@ -17,14 +25,12 @@ describe('CompactionMarker', () => {
expect(screen.getByText('Conversation compacted')).toBeInTheDocument();
});
it('should render custom message from summarizationRequested content', () => {
it('should render custom message from conversationCompacted content', () => {
const message: Message = {
id: '1',
role: 'assistant',
created: 1000,
...default_message,
content: [
{ type: 'text', text: 'Some other content' },
{ type: 'summarizationRequested', msg: 'Custom compaction message' },
{ type: 'conversationCompacted', msg: 'Custom compaction message' },
],
};
@@ -35,9 +41,7 @@ describe('CompactionMarker', () => {
it('should handle empty message content array', () => {
const message: Message = {
id: '1',
role: 'assistant',
created: 1000,
...default_message,
content: [],
};
@@ -48,10 +52,8 @@ describe('CompactionMarker', () => {
it('should handle summarizationRequested content with empty msg', () => {
const message: Message = {
id: '1',
role: 'assistant',
created: 1000,
content: [{ type: 'summarizationRequested', msg: '' }],
...default_message,
content: [{ type: 'conversationCompacted', msg: '' }],
};
render(<CompactionMarker message={message} />);
@@ -62,11 +64,9 @@ describe('CompactionMarker', () => {
it('should handle summarizationRequested content with undefined msg', () => {
const message: Message = {
id: '1',
role: 'assistant',
created: 1000,
...default_message,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content: [{ type: 'summarizationRequested' } as any],
content: [{ type: 'conversationCompacted' } as any],
};
render(<CompactionMarker message={message} />);
@@ -2,7 +2,18 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { ContextManagerProvider, useContextManager } from '../ContextManager';
import * as contextManagement from '../index';
import { ContextManageResponse, Message } from '../../../api';
import { Message } from '../../../api';
const default_message: Message = {
metadata: {
agentVisible: false,
userVisible: false,
},
id: '1',
role: 'assistant',
created: 1000,
content: [],
};
// Mock the context management functions
vi.mock('../index', () => ({
@@ -14,15 +25,11 @@ const mockManageContextFromBackend = vi.mocked(contextManagement.manageContextFr
describe('ContextManager', () => {
const mockMessages: Message[] = [
{
id: '1',
role: 'user',
created: 1000,
...default_message,
content: [{ type: 'text', text: 'Hello' }],
},
{
id: '2',
role: 'assistant',
created: 2000,
...default_message,
content: [{ type: 'text', text: 'Hi there!' }],
},
];
@@ -51,9 +58,6 @@ describe('ContextManager', () => {
expect(result.current.isCompacting).toBe(false);
expect(result.current.compactionError).toBe(null);
expect(typeof result.current.handleAutoCompaction).toBe('function');
expect(typeof result.current.handleManualCompaction).toBe('function');
expect(typeof result.current.hasCompactionMarker).toBe('function');
});
});
@@ -61,10 +65,8 @@ describe('ContextManager', () => {
it('should return true for messages with summarizationRequested content', () => {
const { result } = renderContextManager();
const messageWithMarker: Message = {
id: '1',
role: 'assistant',
created: 1000,
content: [{ type: 'summarizationRequested', msg: 'Compaction marker' }],
...default_message,
content: [{ type: 'conversationCompacted', msg: 'Compaction marker' }],
};
expect(result.current.hasCompactionMarker(messageWithMarker)).toBe(true);
@@ -73,24 +75,20 @@ describe('ContextManager', () => {
it('should return false for messages without summarizationRequested content', () => {
const { result } = renderContextManager();
const regularMessage: Message = {
id: '1',
role: 'user',
created: 1000,
...default_message,
content: [{ type: 'text', text: 'Hello' }],
};
expect(result.current.hasCompactionMarker(regularMessage)).toBe(false);
});
it('should return true for messages with mixed content including summarizationRequested', () => {
it('should return true for messages with mixed content including conversationCompacted', () => {
const { result } = renderContextManager();
const mixedMessage: Message = {
id: '1',
role: 'assistant',
created: 1000,
...default_message,
content: [
{ type: 'text', text: 'Some text' },
{ type: 'summarizationRequested', msg: 'Compaction marker' },
{ type: 'conversationCompacted', msg: 'Compaction marker' },
],
};
@@ -98,220 +96,22 @@ describe('ContextManager', () => {
});
});
describe('handleAutoCompaction', () => {
it('should successfully perform auto compaction with server-provided messages', async () => {
// Mock the backend response with 3 messages: marker, summary, continuation
// Note: Server messages may not have id/created, which will be added by the code
mockManageContextFromBackend.mockResolvedValue({
messages: [
{
role: 'assistant',
content: [
{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' },
],
} as Message,
{
role: 'assistant',
content: [{ type: 'text', text: 'Summary content' }],
} as Message,
{
role: 'assistant',
content: [
{
type: 'text',
text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context',
},
],
} as Message,
],
tokenCounts: [8, 100, 50],
});
const { result } = renderContextManager();
await act(async () => {
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});
expect(mockManageContextFromBackend).toHaveBeenCalledWith({
messages: mockMessages,
manageAction: 'summarize',
sessionId: 'test-session-id',
});
// Expect setMessages to be called with all 3 messages from server
// Note: Server doesn't provide id/created fields, so we don't check for them
expect(mockSetMessages).toHaveBeenCalledTimes(1);
const setMessagesCall = mockSetMessages.mock.calls[0][0];
expect(setMessagesCall).toHaveLength(3);
expect(setMessagesCall[0]).toMatchObject({
role: 'assistant',
content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }],
});
expect(setMessagesCall[1]).toMatchObject({
role: 'assistant',
content: [{ type: 'text', text: 'Summary content' }],
});
expect(setMessagesCall[2]).toMatchObject({
role: 'assistant',
content: [
{
type: 'text',
text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context',
},
],
});
// Fast-forward timers to trigger the append call
act(() => {
vi.advanceTimersByTime(150);
});
// Should append the continuation message (index 2) for auto-compaction
expect(mockAppend).toHaveBeenCalledTimes(1);
const appendedMessage = mockAppend.mock.calls[0][0];
expect(appendedMessage).toMatchObject({
role: 'assistant',
content: [
{
type: 'text',
text: 'The previous message contains a summary that was prepared because a context limit was reached. Do not mention that you read a summary or that conversation summarization occurred Just continue the conversation naturally based on the summarized context',
},
],
});
});
it('should handle compaction errors gracefully', async () => {
const error = new Error('Backend error');
mockManageContextFromBackend.mockRejectedValue(error);
const { result } = renderContextManager();
await act(async () => {
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});
expect(result.current.compactionError).toBe('Backend error');
expect(result.current.isCompacting).toBe(false);
expect(mockSetMessages).toHaveBeenCalledWith([
...mockMessages,
expect.objectContaining({
content: [
{
type: 'summarizationRequested',
msg: 'Compaction failed. Please try again or start a new session.',
},
],
}),
]);
});
it('should set isCompacting state correctly during operation', async () => {
let resolvePromise: (value: ContextManageResponse) => void;
const promise = new Promise<ContextManageResponse>((resolve) => {
resolvePromise = resolve;
});
mockManageContextFromBackend.mockReturnValue(promise);
const { result } = renderContextManager();
// Start compaction
act(() => {
result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});
// Should be compacting
expect(result.current.isCompacting).toBe(true);
expect(result.current.compactionError).toBe(null);
// Resolve the backend call
resolvePromise!({
messages: [
{
role: 'assistant',
content: [{ type: 'text', text: 'Summary content' }],
},
],
tokenCounts: [100, 50],
});
await act(async () => {
await promise;
});
// Should no longer be compacting
expect(result.current.isCompacting).toBe(false);
});
it('preserves display: false for ancestor messages', async () => {
mockManageContextFromBackend.mockResolvedValue({ messages: [], tokenCounts: [] });
const hiddenMessage: Message = {
id: 'hidden-1',
role: 'user',
created: 1500,
content: [{ type: 'text', text: 'Secret' }],
};
const visibleMessage: Message = {
id: 'visible-1',
role: 'assistant',
created: 1600,
content: [{ type: 'text', text: 'Public' }],
};
const messages: Message[] = [hiddenMessage, visibleMessage];
const { result } = renderContextManager();
await act(async () => {
await result.current.handleAutoCompaction(
messages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});
// No server messages -> setMessages called with empty list
expect(mockSetMessages).toHaveBeenCalledWith([]);
expect(mockAppend).not.toHaveBeenCalled();
});
});
describe('handleManualCompaction', () => {
it('should perform compaction with server-provided messages', async () => {
mockManageContextFromBackend.mockResolvedValue({
messages: [
{
role: 'assistant',
...default_message,
content: [
{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' },
{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' },
],
},
{
role: 'assistant',
...default_message,
content: [{ type: 'text', text: 'Manual summary content' }],
},
{
role: 'assistant',
...default_message,
content: [
{
type: 'text',
@@ -346,7 +146,7 @@ describe('ContextManager', () => {
expect(setMessagesCall).toHaveLength(3);
expect(setMessagesCall[0]).toMatchObject({
role: 'assistant',
content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }],
content: [{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' }],
});
expect(setMessagesCall[1]).toMatchObject({
role: 'assistant',
@@ -375,7 +175,7 @@ describe('ContextManager', () => {
mockManageContextFromBackend.mockResolvedValue({
messages: [
{
role: 'assistant',
...default_message,
content: [{ type: 'text', text: 'Manual summary content' }],
},
],
@@ -408,17 +208,17 @@ describe('ContextManager', () => {
mockManageContextFromBackend.mockResolvedValue({
messages: [
{
role: 'assistant',
...default_message,
content: [
{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' },
{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' },
],
},
{
role: 'assistant',
...default_message,
content: [{ type: 'text', text: 'Manual summary content' }],
},
{
role: 'assistant',
...default_message,
content: [
{
type: 'text',
@@ -447,7 +247,7 @@ describe('ContextManager', () => {
expect(setMessagesCall).toHaveLength(3);
expect(setMessagesCall[0]).toMatchObject({
role: 'assistant',
content: [{ type: 'summarizationRequested', msg: 'Conversation compacted and summarized' }],
content: [{ type: 'conversationCompacted', msg: 'Conversation compacted and summarized' }],
});
expect(setMessagesCall[1]).toMatchObject({
role: 'assistant',
@@ -473,65 +273,6 @@ describe('ContextManager', () => {
});
});
describe('Error Handling', () => {
it('should handle backend errors with unknown error type', async () => {
mockManageContextFromBackend.mockRejectedValue('String error');
const { result } = renderContextManager();
await act(async () => {
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});
expect(result.current.compactionError).toBe('Unknown error during compaction');
});
it('should handle missing summary content gracefully with server-provided messages', async () => {
mockManageContextFromBackend.mockResolvedValue({
messages: [
{
role: 'assistant',
content: [
{ type: 'toolResponse', id: 'test', toolResult: { content: 'Not text content' } },
],
} as Message,
],
tokenCounts: [100, 50],
});
const { result } = renderContextManager();
await act(async () => {
await result.current.handleAutoCompaction(
mockMessages,
mockSetMessages,
mockAppend,
'test-session-id'
);
});
// Should complete without error even if content is not text
expect(result.current.isCompacting).toBe(false);
expect(result.current.compactionError).toBe(null);
// Should still set messages from server
expect(mockSetMessages).toHaveBeenCalledTimes(1);
const setMessagesCall = mockSetMessages.mock.calls[0][0];
expect(setMessagesCall).toHaveLength(1);
expect(setMessagesCall[0]).toMatchObject({
role: 'assistant',
content: [
{ type: 'toolResponse', id: 'test', toolResult: { content: 'Not text content' } },
],
});
});
});
describe('Context Provider Error', () => {
it('should throw error when useContextManager is used outside provider', () => {
expect(() => {
+15 -1
View File
@@ -71,25 +71,39 @@ describe('useChatEngine', () => {
describe('onMessageUpdate', () => {
it('should truncate history and append the updated message when a message is edited', () => {
// --- 1. ARRANGE ---
const metadata = {
agentVisible: true,
userVisible: true,
};
const initialMessages: Message[] = [
{ id: '1', role: 'user', content: [{ type: 'text', text: 'First message' }], created: 0 },
{
id: '1',
role: 'user',
content: [{ type: 'text', text: 'First message' }],
created: 0,
metadata,
},
{
id: '2',
role: 'assistant',
content: [{ type: 'text', text: 'First response' }],
created: 1,
metadata,
},
{
id: '3',
role: 'user',
content: [{ type: 'text', text: 'Message to be edited' }],
created: 2,
metadata,
},
{
id: '4',
role: 'assistant',
content: [{ type: 'text', text: 'Response to be deleted' }],
created: 3,
metadata,
},
];
+1
View File
@@ -340,6 +340,7 @@ export const useChatEngine = ({
role: 'user',
created: Date.now(),
content: [],
metadata: { userVisible: true, agentVisible: true },
};
const notification = 'Interrupted by the user to make a correction';
+1
View File
@@ -49,6 +49,7 @@ export function useChatStream({
role: 'user',
content: [{ type: 'text', text: userMessage }],
created: Date.now(),
metadata: { userVisible: true, agentVisible: true },
};
let currentMessages = [...messages, newMessage];
+9 -1
View File
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useId, useReducer, useRef, useState } from 'react';
import useSWR from 'swr';
import { createUserMessage, hasCompletedToolCalls } from '../types/message';
import { Message, Role } from '../api';
import { Conversation, Message, Role } from '../api';
import { getSession, Session } from '../api';
import { ChatState } from '../types/chatState';
@@ -34,6 +34,7 @@ type MessageEvent =
| { type: 'Error'; error: string }
| { type: 'Finish'; reason: string }
| { type: 'ModelChange'; model: string; mode: string }
| { type: 'UpdateConversation'; conversation: Conversation }
| NotificationEvent;
export interface UseMessageStreamOptions {
@@ -328,6 +329,11 @@ export function useMessageStream({
break;
}
case 'UpdateConversation': {
setMessages(parsedEvent.conversation);
break;
}
case 'Error': {
// Always throw the error so it gets caught and sets the error state
// This ensures the retry UI appears for ALL errors
@@ -384,6 +390,7 @@ export function useMessageStream({
return currentMessages;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[mutate, mutateChatState, onFinish, onError, forceUpdate, setError]
);
@@ -580,6 +587,7 @@ export function useMessageStream({
id: generateMessageId(),
role: 'user' as const,
created: Math.floor(Date.now() / 1000),
metadata: { userVisible: true, agentVisible: true },
content: [
{
type: 'toolResponse' as const,
+3 -54
View File
@@ -1,4 +1,4 @@
import { Content, Message, ToolConfirmationRequest, ToolRequest, ToolResponse } from '../api';
import { Message, ToolConfirmationRequest, ToolRequest, ToolResponse } from '../api';
export type ToolRequestMessageContent = ToolRequest & { type: 'toolRequest' };
export type ToolResponseMessageContent = ToolResponse & { type: 'toolResponse' };
@@ -9,58 +9,7 @@ export function createUserMessage(text: string): Message {
role: 'user',
created: Math.floor(Date.now() / 1000),
content: [{ type: 'text', text }],
};
}
export function createAssistantMessage(text: string): Message {
return {
id: generateId(),
role: 'assistant',
created: Math.floor(Date.now() / 1000),
content: [{ type: 'text', text }],
};
}
export function createToolRequestMessage(
id: string,
toolName: string,
args: Record<string, unknown>
): Message {
return {
id: generateId(),
role: 'assistant',
created: Math.floor(Date.now() / 1000),
content: [
{
type: 'toolRequest',
id,
toolCall: {
status: 'success',
value: {
name: toolName,
arguments: args,
},
},
},
],
};
}
export function createToolResponseMessage(id: string, result: Content[]): Message {
return {
id: generateId(),
role: 'user',
created: Math.floor(Date.now() / 1000),
content: [
{
type: 'toolResponse',
id,
toolResult: {
status: 'success',
value: result,
},
},
],
metadata: { userVisible: true, agentVisible: true },
};
}
@@ -79,6 +28,7 @@ export function createToolErrorResponseMessage(id: string, error: string): Messa
},
},
],
metadata: { userVisible: true, agentVisible: true },
};
}
@@ -90,7 +40,6 @@ export function getTextContent(message: Message): string {
return message.content
.map((content) => {
if (content.type === 'text') return content.text;
if (content.type === 'contextLengthExceeded') return content.msg;
return '';
})
.join('');