@@ -2724,6 +2724,44 @@ const BUILTIN_RESPONSE_HANDLER_EVALUATORS: readonly ResponseHandlerEvaluator[] =
27242724 ] ,
27252725 } ) ,
27262726 } ,
2727+ {
2728+ name : "core.contextual_identity_lookup_requires_recall" ,
2729+ description :
2730+ "Routes short chat-entity identity lookups through memory/messaging instead of letting Stage 1 guess from the simple shortcut." ,
2731+ priority : 20 ,
2732+ shouldRun : ( { message, messageHandler } ) =>
2733+ ! isSubAgentCompletionArtifact ( message ) &&
2734+ isSimpleMessageHandlerShortcut ( messageHandler ) &&
2735+ extractContextualIdentityLookupSubject (
2736+ getUserMessageText ( message ) ?? "" ,
2737+ ) !== null ,
2738+ evaluate : ( { runtime, message } ) => {
2739+ const subject = extractContextualIdentityLookupSubject (
2740+ getUserMessageText ( message ) ?? "" ,
2741+ ) ;
2742+ const messageAction = findRuntimeActionByNames ( runtime , [ "MESSAGE" ] ) ;
2743+ return {
2744+ setContexts : [ "general" , "memory" , "messaging" ] ,
2745+ ...( messageAction
2746+ ? {
2747+ addCandidateActions : [ messageAction . name ] ,
2748+ }
2749+ : { } ) ,
2750+ clearReply : true ,
2751+ addContextSlices : [
2752+ [
2753+ "identity_lookup_policy:" ,
2754+ `The current user is asking who/what "${ subject ?? "the named subject" } " is in context.` ,
2755+ "Ground the answer in recent messages, memory, or message-search results before identifying the subject." ,
2756+ "If recalled context does not identify a chat-local subject, say that plainly instead of inventing a relationship; only use public knowledge when the subject is clearly a public entity." ,
2757+ ] . join ( "\n" ) ,
2758+ ] ,
2759+ debug : [
2760+ `short identity lookup for "${ subject ?? "unknown" } "; routing through recall context before answering` ,
2761+ ] ,
2762+ } ;
2763+ } ,
2764+ } ,
27272765 ] ;
27282766
27292767/**
@@ -3490,6 +3528,122 @@ function looksLikeCompleteDirectReply(replyText: string): boolean {
34903528 ) ;
34913529}
34923530
3531+ function isSimpleMessageHandlerShortcut (
3532+ messageHandler : MessageHandlerResult ,
3533+ ) : boolean {
3534+ if ( messageHandler . processMessage !== "RESPOND" ) return false ;
3535+ if ( messageHandler . plan . requiresTool === true ) return false ;
3536+ const contexts = messageHandler . plan . contexts ?? [ ] ;
3537+ const nonSimpleContexts = contexts . filter (
3538+ ( context ) => context !== SIMPLE_CONTEXT_ID ,
3539+ ) ;
3540+ return (
3541+ nonSimpleContexts . length === 0 &&
3542+ ( messageHandler . plan . candidateActions ?. length ?? 0 ) === 0
3543+ ) ;
3544+ }
3545+
3546+ const CONTEXTUAL_IDENTITY_LOOKUP_PREFIXES = [
3547+ "who is " ,
3548+ "who are " ,
3549+ "who was " ,
3550+ "who were " ,
3551+ "who's " ,
3552+ "whos " ,
3553+ "what is " ,
3554+ "what was " ,
3555+ "what's " ,
3556+ "whats " ,
3557+ "tell me about " ,
3558+ "do you know " ,
3559+ "do you remember " ,
3560+ "what do you know about " ,
3561+ ] as const ;
3562+
3563+ const CONTEXTUAL_IDENTITY_PRONOUN_SUBJECTS = new Set ( [
3564+ "he" ,
3565+ "her" ,
3566+ "him" ,
3567+ "i" ,
3568+ "it" ,
3569+ "me" ,
3570+ "she" ,
3571+ "that" ,
3572+ "them" ,
3573+ "they" ,
3574+ "this" ,
3575+ "us" ,
3576+ "we" ,
3577+ "you" ,
3578+ ] ) ;
3579+
3580+ function removeLeadingPlatformAddress ( text : string ) : string {
3581+ let cleaned = text
3582+ . replace ( / < @ ! ? \d + > / gu, " " )
3583+ . replace ( / \s + / gu, " " )
3584+ . trim ( ) ;
3585+ const displayAddressRe = / (?: ^ | [ \s : ] ) [ ^ : \n ] { 0 , 96 } \( @ \d + \) \s + / gu;
3586+ let displayAddressEnd = - 1 ;
3587+ for (
3588+ let displayAddressMatch = displayAddressRe . exec ( cleaned ) ;
3589+ displayAddressMatch !== null ;
3590+ displayAddressMatch = displayAddressRe . exec ( cleaned )
3591+ ) {
3592+ displayAddressEnd = displayAddressRe . lastIndex ;
3593+ }
3594+ if ( displayAddressEnd > 0 ) {
3595+ cleaned = cleaned . slice ( displayAddressEnd ) . trim ( ) ;
3596+ }
3597+ return cleaned ;
3598+ }
3599+
3600+ function cleanContextualLookupSubject ( subject : string ) : string {
3601+ return subject
3602+ . replace ( / [ \s ? . ! ] + $ / gu, "" )
3603+ . replace ( / ^ [ " ' ` “ ” ‘ ’ ] + | [ " ' ` “ ” ‘ ’ ] + $ / gu, "" )
3604+ . replace (
3605+ / \s + (?: i n | f r o m | o n ) \s + (?: t h i s \s + ) ? (?: c h a t | c h a n n e l | t h r e a d | s e r v e r ) $ / iu,
3606+ "" ,
3607+ )
3608+ . trim ( ) ;
3609+ }
3610+
3611+ function isShortContextualLookupSubject ( subject : string ) : boolean {
3612+ const normalized = subject . toLowerCase ( ) ;
3613+ if ( ! normalized || CONTEXTUAL_IDENTITY_PRONOUN_SUBJECTS . has ( normalized ) ) {
3614+ return false ;
3615+ }
3616+ const words = normalized . split ( / \s + / u) . filter ( Boolean ) ;
3617+ if ( words . length === 0 || words . length > 3 ) return false ;
3618+ if ( subject . length > 48 ) return false ;
3619+ if ( / ^ [ 0 - 9 \s . , : + * / = ( ) - ] + $ / u. test ( subject ) ) return false ;
3620+ const looksLikeLocalName =
3621+ / [ @ _ \- 0 - 9 ] / u. test ( subject ) ||
3622+ / \b (?: b o t | a g e n t | a i ) \b / iu. test ( subject ) ||
3623+ / ^ [ a - z ] [ a - z 0 - 9 _ - ] { 6 , } $ / u. test ( subject ) ;
3624+ return looksLikeLocalName ;
3625+ }
3626+
3627+ function lastNonEmptyLine ( text : string ) : string {
3628+ const lines = text . replace ( / \r \n / g, "\n" ) . split ( "\n" ) ;
3629+ for ( let index = lines . length - 1 ; index >= 0 ; index -= 1 ) {
3630+ const line = lines [ index ] ?. trim ( ) ;
3631+ if ( line ) return line ;
3632+ }
3633+ return text . trim ( ) ;
3634+ }
3635+
3636+ function extractContextualIdentityLookupSubject ( text : string ) : string | null {
3637+ const cleaned = removeLeadingPlatformAddress ( lastNonEmptyLine ( text ) ) ;
3638+ const lower = cleaned . toLowerCase ( ) ;
3639+ for ( const prefix of CONTEXTUAL_IDENTITY_LOOKUP_PREFIXES ) {
3640+ if ( ! lower . startsWith ( prefix ) ) continue ;
3641+ const subject = cleanContextualLookupSubject ( cleaned . slice ( prefix . length ) ) ;
3642+ return isShortContextualLookupSubject ( subject ) ? subject : null ;
3643+ }
3644+ return null ;
3645+ }
3646+
34933647function shouldPreferCompleteDirectReply ( args : {
34943648 replyText : string ;
34953649 currentMessageText : string ;
@@ -5168,11 +5322,17 @@ export async function runV5MessageRuntimeStage1(args: {
51685322 preselectedActions : exposedPlannerActions ,
51695323 actionSurface,
51705324 } ) ;
5325+ const responseHandlerContextSlices = stringArrayProperty (
5326+ ( messageHandler . plan as { contextSlices ?: unknown } ) . contextSlices ,
5327+ ) ;
51715328 const plannerContextWithDecision = appendContextEvent ( plannerContext , {
51725329 id : `message-handler:${ messageHandlerEndedAt } ` ,
51735330 type : "message_handler" ,
51745331 source : "message-service" ,
51755332 createdAt : messageHandlerEndedAt ,
5333+ ...( responseHandlerContextSlices . length > 0
5334+ ? { content : responseHandlerContextSlices . join ( "\n\n" ) }
5335+ : { } ) ,
51765336 metadata : {
51775337 processMessage : messageHandler . processMessage ,
51785338 plan : {
@@ -5182,6 +5342,9 @@ export async function runV5MessageRuntimeStage1(args: {
51825342 : { } ) ,
51835343 candidateActions : getMessageHandlerCandidateActions ( messageHandler ) ,
51845344 parentActionHints : getMessageHandlerParentActionHints ( messageHandler ) ,
5345+ ...( responseHandlerContextSlices . length > 0
5346+ ? { contextSlices : responseHandlerContextSlices }
5347+ : { } ) ,
51855348 ...( messageHandler . plan . reply !== undefined
51865349 ? { reply : messageHandler . plan . reply }
51875350 : { } ) ,
0 commit comments