@@ -20,6 +20,7 @@ import (
2020 "context"
2121 "errors"
2222 "fmt"
23+ "io"
2324 "strings"
2425
2526 "github.com/cloudwego/eino/adk"
@@ -35,6 +36,10 @@ const (
3536 projectEinoAssistantSummaryContextMessages = 128
3637 projectEinoAssistantSummaryContextTokens = 24000
3738 projectEinoAssistantSummaryInstruction = "Summarize this App Studio project session for the next builder turn. Preserve user requirements, accepted plans, files touched or inspected, unresolved questions, repository/runtime state, and any constraints. Keep it concise and operational."
39+
40+ // Bundle search is for App Studio's full product toolbox. Smaller injected
41+ // tool sets stay direct so focused permission/resume flows keep their shape.
42+ projectEinoAssistantBundleSearchMinTools = 4
3843)
3944
4045type projectEinoAssistantEngine struct {
@@ -181,6 +186,8 @@ func (e projectEinoAssistantEngine) newAgent(ctx context.Context, req projectAss
181186}
182187
183188func projectEinoAssistantToolSearchSets (ctx context.Context , tools []einotool.BaseTool ) ([]einotool.BaseTool , []einotool.BaseTool , error ) {
189+ infos := make ([]* schema.ToolInfo , 0 , len (tools ))
190+ searchCandidateCount := 0
184191 staticTools := make ([]einotool.BaseTool , 0 , len (tools ))
185192 dynamicTools := make ([]einotool.BaseTool , 0 , len (tools ))
186193 for _ , tool := range tools {
@@ -191,7 +198,20 @@ func projectEinoAssistantToolSearchSets(ctx context.Context, tools []einotool.Ba
191198 if err != nil {
192199 return nil , nil , err
193200 }
194- if projectEinoAssistantToolUsesSearch (info ) {
201+ infos = append (infos , info )
202+ if projectEinoAssistantToolCanUseSearch (info ) {
203+ searchCandidateCount ++
204+ }
205+ }
206+ useBundleSearch := searchCandidateCount >= projectEinoAssistantBundleSearchMinTools
207+ infoIndex := 0
208+ for _ , tool := range tools {
209+ if tool == nil {
210+ continue
211+ }
212+ info := infos [infoIndex ]
213+ infoIndex ++
214+ if projectEinoAssistantToolUsesSearch (info , useBundleSearch ) {
195215 dynamicTools = append (dynamicTools , tool )
196216 continue
197217 }
@@ -200,10 +220,21 @@ func projectEinoAssistantToolSearchSets(ctx context.Context, tools []einotool.Ba
200220 return staticTools , dynamicTools , nil
201221}
202222
203- func projectEinoAssistantToolUsesSearch (info * schema.ToolInfo ) bool {
223+ func projectEinoAssistantToolCanUseSearch (info * schema.ToolInfo ) bool {
204224 if info == nil || info .Extra == nil {
205225 return false
206226 }
227+ bundle , _ := info .Extra ["bundle" ].(string )
228+ return projectAssistantToolBundle (bundle ) != projectAssistantToolBundleCollaboration
229+ }
230+
231+ func projectEinoAssistantToolUsesSearch (info * schema.ToolInfo , useBundleSearch bool ) bool {
232+ if info == nil || info .Extra == nil {
233+ return false
234+ }
235+ if useBundleSearch && projectEinoAssistantToolCanUseSearch (info ) {
236+ return true
237+ }
207238 risk , _ := info .Extra ["risk" ].(string )
208239 return projectAssistantToolRisk (risk ) == projectAssistantToolRiskRead
209240}
@@ -231,7 +262,7 @@ func (e projectEinoAssistantEngine) runProjectAssistantTurnLoop(
231262 if len (items ) == 0 {
232263 return nil , errors .New ("eino turn loop received no work" )
233264 }
234- input , err := projectEinoAssistantInputMessages (req , runState )
265+ input , err := projectEinoAssistantInputMessages (loopCtx , req , runState )
235266 if err != nil {
236267 return nil , err
237268 }
@@ -376,14 +407,19 @@ func (e projectEinoAssistantEngine) collectProjectAssistantTurnEvents(
376407 outcome .receivedOutput = true
377408 continue
378409 }
379- if event .Output .MessageOutput == nil {
410+ messageOutput := event .Output .MessageOutput
411+ if messageOutput == nil {
380412 continue
381413 }
382- msg , err := event . Output . MessageOutput . GetMessage ( )
414+ msg , err := projectEinoAssistantMessageOutput ( eventCtx , messageOutput , req . StreamCallbacks )
383415 if err != nil {
384416 return err
385417 }
386- if msg != nil && msg .Role == schema .Assistant && strings .TrimSpace (msg .Content ) != "" {
418+ role := messageOutput .Role
419+ if role == "" && msg != nil {
420+ role = msg .Role
421+ }
422+ if msg != nil && role == schema .Assistant && strings .TrimSpace (msg .Content ) != "" && ! projectEinoAssistantMessageSuppressesPublicContent (msg ) {
387423 outcome .result .Content = msg .Content
388424 outcome .receivedOutput = true
389425 }
@@ -394,6 +430,80 @@ func (e projectEinoAssistantEngine) collectProjectAssistantTurnEvents(
394430 return nil
395431}
396432
433+ func projectEinoAssistantMessageOutput (
434+ ctx context.Context ,
435+ output * adk.TypedMessageVariant [* schema.Message ],
436+ streamCallbacks projectAssistantStreamCallbacks ,
437+ ) (* schema.Message , error ) {
438+ if output == nil {
439+ return nil , nil
440+ }
441+ if ! output .IsStreaming {
442+ return output .Message , nil
443+ }
444+ if output .MessageStream == nil {
445+ return nil , errors .New ("eino assistant stream event missing message stream" )
446+ }
447+ defer output .MessageStream .Close ()
448+
449+ var chunks []* schema.Message
450+ for {
451+ if err := ctx .Err (); err != nil {
452+ return nil , err
453+ }
454+ msg , err := output .MessageStream .Recv ()
455+ if errors .Is (err , io .EOF ) {
456+ break
457+ }
458+ if err != nil {
459+ return nil , err
460+ }
461+ if msg == nil {
462+ continue
463+ }
464+ chunks = append (chunks , msg )
465+ }
466+ msg , err := schema .ConcatMessages (chunks )
467+ if err != nil {
468+ return nil , err
469+ }
470+ if output .Role == schema .Assistant && ! projectEinoAssistantMessageSuppressesPublicContent (msg ) && streamCallbacks .OnChunk != nil {
471+ for _ , chunk := range chunks {
472+ if chunk != nil && chunk .Content != "" {
473+ streamCallbacks .OnChunk (chunk .Content )
474+ }
475+ }
476+ }
477+ return msg , nil
478+ }
479+
480+ func projectEinoAssistantMessageHasToolCalls (msg * schema.Message ) bool {
481+ return msg != nil && len (msg .ToolCalls ) > 0
482+ }
483+
484+ func projectEinoAssistantMessageSuppressesPublicContent (msg * schema.Message ) bool {
485+ return projectEinoAssistantMessageHasToolCalls (msg ) && projectEinoAssistantToolNarration (msg .Content )
486+ }
487+
488+ func projectEinoAssistantToolNarration (content string ) bool {
489+ content = strings .TrimSpace (strings .ToLower (content ))
490+ if content == "" {
491+ return false
492+ }
493+ for _ , prefix := range []string {
494+ "i will " ,
495+ "i'll " ,
496+ "i am going to " ,
497+ "i'm going to " ,
498+ "let me " ,
499+ } {
500+ if strings .HasPrefix (content , prefix ) {
501+ return true
502+ }
503+ }
504+ return false
505+ }
506+
397507func (e projectEinoAssistantEngine ) projectAssistantToolLoopFinalAnswer (
398508 ctx context.Context ,
399509 req projectAssistantRunRequest ,
@@ -571,12 +681,15 @@ func projectEinoFollowUpInterruptInfoFromEvent(interrupted *adk.InterruptInfo) (
571681 return nil , "" , false
572682}
573683
574- func projectEinoAssistantInputMessages (req projectAssistantRunRequest , runState * projectEinoAssistantRunState ) ([]adk.Message , error ) {
684+ func projectEinoAssistantInputMessages (ctx context. Context , req projectAssistantRunRequest , runState * projectEinoAssistantRunState ) ([]adk.Message , error ) {
575685 var chatMessages []chatMessage
576686 if req .Continuation != nil && len (req .Continuation .Messages ) > 0 {
577687 chatMessages = cloneChatMessages (req .Continuation .Messages )
578688 } else {
579689 chatMessages = projectPromptMessages (req .Project , req .Repository , req .History )
690+ if snapshot , ok := projectEinoAssistantSessionContextMessage (ctx , req , runState ); ok {
691+ chatMessages = append (chatMessages , snapshot )
692+ }
580693 if prompt := runState .ToolPrompt (); prompt != "" {
581694 chatMessages = append (chatMessages , chatMessage {Role : "system" , Content : prompt })
582695 }
0 commit comments