-
Notifications
You must be signed in to change notification settings - Fork 110
Conversation history returns MessageSelect[] and formats at call site #2743
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ import { | |
| generateId, | ||
| getConversationHistory, | ||
| getLedgerArtifacts, | ||
| type MessageSelect, | ||
| type ResolvedRef, | ||
| } from '@inkeep/agents-core'; | ||
| import { trace } from '@opentelemetry/api'; | ||
|
|
@@ -146,7 +147,7 @@ export async function getScopedHistory({ | |
| conversationId: string; | ||
| filters?: ConversationScopeOptions; | ||
| options?: ConversationHistoryConfig; | ||
| }): Promise<any[]> { | ||
| }): Promise<MessageSelect[]> { | ||
| try { | ||
| // First, get ALL messages to find the latest compression summary | ||
| // IMPORTANT: Always include internal messages and disable truncation to ensure tool results are available | ||
|
|
@@ -173,7 +174,7 @@ export async function getScopedHistory({ | |
|
|
||
| const limit = options?.limit; | ||
|
|
||
| let messages: any[]; | ||
| let messages: MessageSelect[]; | ||
| if (latestCompressionSummary) { | ||
| // Get the summary + all messages after it | ||
| const summaryDate = new Date(latestCompressionSummary.createdAt); | ||
|
|
@@ -427,7 +428,7 @@ export async function getConversationHistoryWithCompression({ | |
| baseModel?: any; | ||
| streamRequestId?: string; | ||
| fullContextSize?: number; | ||
| }): Promise<string> { | ||
| }): Promise<MessageSelect[]> { | ||
| const historyOptions = options ?? createDefaultConversationHistoryConfig(); | ||
|
|
||
| // IMPORTANT: For conversation compression, we MUST include internal messages (tool results) | ||
|
|
@@ -459,7 +460,7 @@ export async function getConversationHistoryWithCompression({ | |
| } | ||
|
|
||
| if (!messagesToFormat.length) { | ||
| return ''; | ||
| return []; | ||
| } | ||
|
|
||
| // Replace tool-result content with compact artifact references BEFORE compression. | ||
|
|
@@ -476,38 +477,45 @@ export async function getConversationHistoryWithCompression({ | |
| scopes: { tenantId, projectId }, | ||
| toolCallIds, | ||
| }); | ||
| const artifactsByToolCallId = new Map( | ||
| artifacts.filter((a) => a.toolCallId).map((a) => [a.toolCallId as string, a]) | ||
| ); | ||
| const artifactsByToolCallId = new Map<string, Artifact[]>(); | ||
| for (const artifact of artifacts) { | ||
| if (!artifact.toolCallId) continue; | ||
| const existing = artifactsByToolCallId.get(artifact.toolCallId) || []; | ||
| existing.push(artifact); | ||
| artifactsByToolCallId.set(artifact.toolCallId, existing); | ||
| } | ||
| if (artifactsByToolCallId.size > 0) { | ||
| messagesToFormat = messagesToFormat.map((msg) => { | ||
| if (msg.messageType !== 'tool-result') return msg; | ||
| const tcId = msg.metadata?.a2a_metadata?.toolCallId; | ||
| const artifact = tcId ? artifactsByToolCallId.get(tcId) : undefined; | ||
| if (!artifact) return msg; | ||
| const relatedArtifacts = tcId ? artifactsByToolCallId.get(tcId) : undefined; | ||
| if (!relatedArtifacts || relatedArtifacts.length === 0) return msg; | ||
| const toolArgs = msg.metadata?.a2a_metadata?.toolArgs; | ||
| const rawArgs = toolArgs ? JSON.stringify(toolArgs) : undefined; | ||
| const argsStr = | ||
| rawArgs && rawArgs.length > 300 ? `${rawArgs.slice(0, 300)}...[truncated]` : rawArgs; | ||
| const dataPart = artifact.parts?.find( | ||
| (p): p is Extract<(typeof artifact.parts)[number], { kind: 'data' }> => | ||
| p.kind === 'data' | ||
| ); | ||
| const summaryValue = dataPart?.data?.summary; | ||
| const rawSummary = summaryValue ? JSON.stringify(summaryValue) : undefined; | ||
| const summaryDataStr = | ||
| rawSummary && rawSummary.length > 1000 | ||
| ? `${rawSummary.slice(0, 1000)}...[truncated]` | ||
| : rawSummary; | ||
| const refParts = [ | ||
| `Artifact: "${artifact.name ?? artifact.artifactId}" (id: ${artifact.artifactId})`, | ||
| ]; | ||
| if (argsStr) refParts.push(`args: ${argsStr}`); | ||
| if (artifact.description) refParts.push(`description: ${artifact.description}`); | ||
| if (summaryDataStr) refParts.push(`summary: ${summaryDataStr}`); | ||
| const refParts = relatedArtifacts.map((artifact) => { | ||
| const dataPart = artifact.parts?.find( | ||
| (p): p is Extract<(typeof artifact.parts)[number], { kind: 'data' }> => | ||
| p.kind === 'data' | ||
| ); | ||
| const summaryValue = dataPart?.data?.summary; | ||
| const rawSummary = summaryValue ? JSON.stringify(summaryValue) : undefined; | ||
| const summaryDataStr = | ||
| rawSummary && rawSummary.length > 1000 | ||
| ? `${rawSummary.slice(0, 1000)}...[truncated]` | ||
| : rawSummary; | ||
| const artifactParts = [ | ||
| `Artifact: "${artifact.name ?? artifact.artifactId}" (id: ${artifact.artifactId})`, | ||
| ]; | ||
| if (argsStr) artifactParts.push(`args: ${argsStr}`); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Minor: Args duplicated for each artifact sharing a toolCallId Issue: When multiple artifacts share the same Why: This unnecessarily bloats token count in the conversation history. For tool calls that produce multiple artifacts (e.g., multiple images or documents), the args string could be repeated 2-10x. Fix: Consider moving args to a single prefix line before the artifact list: const argsLine = argsStr ? `Tool call args: ${argsStr}\n` : '';
const artifactRefs = relatedArtifacts.map((artifact) => {
// ... build artifact-specific parts WITHOUT args
});
return {
...msg,
content: { text: argsLine + artifactRefs.join('\n') },
};Refs: |
||
| if (artifact.description) artifactParts.push(`description: ${artifact.description}`); | ||
| if (summaryDataStr) artifactParts.push(`summary: ${summaryDataStr}`); | ||
| return `[${artifactParts.join(' | ')}]`; | ||
| }); | ||
| return { | ||
| ...msg, | ||
| content: { text: `[${refParts.join(' | ')}]` }, | ||
| content: { text: refParts.join('\n') }, | ||
| }; | ||
| }); | ||
| } | ||
|
|
@@ -561,7 +569,7 @@ export async function getConversationHistoryWithCompression({ | |
| } | ||
| } | ||
|
|
||
| return formatMessagesAsConversationHistory(messagesToFormat); | ||
| return messagesToFormat; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -894,10 +902,12 @@ export function reconstructMessageText(msg: any): string { | |
|
|
||
| return parts | ||
| .map((part: any) => { | ||
| if (part.type === 'text') { | ||
| const partKind = part.kind ?? part.type; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💭 Consider: Defensive fallback for Issue: This fallback suggests there are two different part schemas in play: the canonical Why: Without clarity on the source of Fix: If both are legitimate variants (e.g., legacy data or external formats), add a brief inline comment: // Support both 'kind' (canonical schema) and 'type' (legacy/external format)
const partKind = part.kind ?? part.type;If |
||
|
|
||
| if (partKind === 'text') { | ||
| return part.text ?? ''; | ||
| } | ||
| if (part.type === 'data') { | ||
| if (partKind === 'data') { | ||
| try { | ||
| const data = typeof part.data === 'string' ? JSON.parse(part.data) : part.data; | ||
| if (data?.artifactId && data?.toolCallId) { | ||
|
|
@@ -912,9 +922,9 @@ export function reconstructMessageText(msg: any): string { | |
| .join(''); | ||
| } | ||
|
|
||
| function formatMessagesAsConversationHistory(messages: any[]): string { | ||
| export function formatMessagesAsConversationHistory(messages: MessageSelect[]): string { | ||
| const formattedHistory = messages | ||
| .map((msg: any) => { | ||
| .map((msg: MessageSelect) => { | ||
| let roleLabel: string; | ||
|
|
||
| if (msg.role === 'user') { | ||
|
|
@@ -939,8 +949,13 @@ function formatMessagesAsConversationHistory(messages: any[]): string { | |
| roleLabel = msg.role || 'system'; | ||
| } | ||
|
|
||
| return `${roleLabel}: """${reconstructMessageText(msg)}"""`; | ||
| const reconstructedMessage = reconstructMessageText(msg); | ||
| if (!reconstructedMessage) { | ||
| return null; | ||
| } | ||
| return `${roleLabel}: """${reconstructedMessage}"""`; | ||
| }) | ||
| .filter((line): line is string => line !== null) | ||
|
Comment on lines
+952
to
+958
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Comment on lines
+952
to
+958
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💭 Consider: Behavioral change - empty messages now filtered out Issue: The new implementation filters out messages with empty reconstructed text (returning Why: This could affect downstream consumers that rely on message boundaries (e.g., compression logic counting messages) or debugging traces. If intentional, it's worth documenting. Fix: If intentional, consider adding a test case validating the filtering behavior and noting this in the PR description as a deliberate improvement. |
||
| .join('\n'); | ||
|
|
||
| return `<conversation_history>\n${formattedHistory}\n</conversation_history>\n`; | ||
|
Comment on lines
925
to
961
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The newly exported
Since |
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test verifies both artifact IDs are present but doesn't assert the
\njoin between them. If the join separator were accidentally changed to empty string or space, this test would still pass. Consider: