Skip to content

Commit 2b071aa

Browse files
committed
preemptive generation based on python library
1 parent daadcb4 commit 2b071aa

File tree

1 file changed

+107
-0
lines changed

1 file changed

+107
-0
lines changed

agents/src/llm/chat_context.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,113 @@ export class ChatContext {
499499
return await toChatCtx(format, this, injectDummyUserMessage);
500500
}
501501

502+
/**
503+
* Compare this ChatContext with another for logical equivalence.
504+
* Unlike strict equality, this method:
505+
* - Ignores timestamps (createdAt fields)
506+
* - Ignores other volatile metadata
507+
* - Focuses on content: compares IDs, types, and payload
508+
*
509+
* This is useful for detecting if the conversation content has changed,
510+
* for example when validating preemptive generation results.
511+
*
512+
* @param other - The ChatContext to compare with
513+
* @returns true if both contexts contain the same sequence of items with matching essential fields
514+
*/
515+
isEquivalent(other: ChatContext): boolean {
516+
// Same object reference
517+
if (this === other) {
518+
return true;
519+
}
520+
521+
// Different lengths
522+
if (this._items.length !== other._items.length) {
523+
return false;
524+
}
525+
526+
// Compare each item pair
527+
for (let i = 0; i < this._items.length; i++) {
528+
const a = this._items[i]!;
529+
const b = other._items[i]!;
530+
531+
// IDs and types must match
532+
if (a.id !== b.id || a.type !== b.type) {
533+
return false;
534+
}
535+
536+
// Type-specific field comparison
537+
if (a.type === 'message' && b.type === 'message') {
538+
// Compare role, content, and interrupted status (not timestamp)
539+
if (a.role !== b.role || a.interrupted !== b.interrupted) {
540+
return false;
541+
}
542+
543+
// Compare content arrays
544+
if (a.content.length !== b.content.length) {
545+
return false;
546+
}
547+
548+
for (let j = 0; j < a.content.length; j++) {
549+
const ca = a.content[j]!;
550+
const cb = b.content[j]!;
551+
552+
// Both are strings
553+
if (typeof ca === 'string' && typeof cb === 'string') {
554+
if (ca !== cb) {
555+
return false;
556+
}
557+
}
558+
// Both are objects
559+
else if (typeof ca === 'object' && typeof cb === 'object') {
560+
if (ca.type !== cb.type) {
561+
return false;
562+
}
563+
564+
if (ca.type === 'image_content' && cb.type === 'image_content') {
565+
// Compare essential image fields (not cache)
566+
if (
567+
ca.id !== cb.id ||
568+
ca.image !== cb.image ||
569+
ca.inferenceDetail !== cb.inferenceDetail ||
570+
ca.inferenceWidth !== cb.inferenceWidth ||
571+
ca.inferenceHeight !== cb.inferenceHeight ||
572+
ca.mimeType !== cb.mimeType
573+
) {
574+
return false;
575+
}
576+
} else if (ca.type === 'audio_content' && cb.type === 'audio_content') {
577+
// Compare audio transcript (frames comparison would be too expensive)
578+
if (ca.transcript !== cb.transcript) {
579+
return false;
580+
}
581+
}
582+
}
583+
// Mismatched types
584+
else {
585+
return false;
586+
}
587+
}
588+
} else if (a.type === 'function_call' && b.type === 'function_call') {
589+
// Compare name, callId, and args (not timestamp)
590+
if (a.name !== b.name || a.callId !== b.callId || a.args !== b.args) {
591+
return false;
592+
}
593+
} else if (a.type === 'function_call_output' && b.type === 'function_call_output') {
594+
// Compare name, callId, output, and isError (not timestamp)
595+
if (
596+
a.name !== b.name ||
597+
a.callId !== b.callId ||
598+
a.output !== b.output ||
599+
a.isError !== b.isError
600+
) {
601+
return false;
602+
}
603+
}
604+
}
605+
606+
return true;
607+
}
608+
502609
/**
503610
* Internal helper used by `truncate` & `addMessage` to find the correct
504611
* insertion index for a timestamp so the list remains sorted.

0 commit comments

Comments
 (0)