mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 19:47:10 +08:00
fix(forwarder): handle Cursor summarize actions
Map Cursor's protocol-level SummarizeAction to manual context compaction while keeping the legacy /compact directive disabled. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -77,7 +77,7 @@ func (service *Service) maybeCompactBeforeProvider(stream *ActiveStream, convers
|
|||||||
if service == nil || stream == nil || conversation == nil {
|
if service == nil || stream == nil || conversation == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
manualInstruction, manual := parseManualCompactionDirective(stream.LatestUserText)
|
manualInstruction, manual := streamManualCompactionDirective(stream)
|
||||||
plan, err := service.buildCompactionPlan(stream, conversation, compiled, manual, manualInstruction)
|
plan, err := service.buildCompactionPlan(stream, conversation, compiled, manual, manualInstruction)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@@ -827,7 +827,7 @@ func buildFallbackCompactionSummary(plan *PendingCompaction) string {
|
|||||||
sections = append(sections, "Compaction note:\n"+truncateCompactionText(plan.HookMessage, 800))
|
sections = append(sections, "Compaction note:\n"+truncateCompactionText(plan.HookMessage, 800))
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(plan.ManualInstruction) != "" {
|
if strings.TrimSpace(plan.ManualInstruction) != "" {
|
||||||
sections = append(sections, "Manual compact instruction:\n"+truncateCompactionText(plan.ManualInstruction, 800))
|
sections = append(sections, "Manual summarize instruction:\n"+truncateCompactionText(plan.ManualInstruction, 800))
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(truncateCompactionText(strings.Join(sections, "\n\n"), compactionSummaryMaxChars))
|
return strings.TrimSpace(truncateCompactionText(strings.Join(sections, "\n\n"), compactionSummaryMaxChars))
|
||||||
}
|
}
|
||||||
@@ -875,13 +875,62 @@ func (service *Service) resolveCompactionReserveTokens(modelID string) int64 {
|
|||||||
return compactionAutoReserveTokens
|
return compactionAutoReserveTokens
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseManualCompactionRequest(userMessage *agentv1.UserMessage) (string, bool) {
|
||||||
|
if userMessage == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
userText := strings.TrimSpace(userMessage.GetText())
|
||||||
|
if instruction, ok := parseManualCompactionDirective(userText); ok {
|
||||||
|
return instruction, true
|
||||||
|
}
|
||||||
|
if userText != "" {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
selectedContext := userMessage.GetSelectedContext()
|
||||||
|
if selectedContext == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for _, command := range selectedContext.GetCursorCommands() {
|
||||||
|
if !isCursorSummarizeCommand(command) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instruction, _ := parseManualCompactionDirective(command.GetContent())
|
||||||
|
return instruction, true
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func streamManualCompactionDirective(stream *ActiveStream) (string, bool) {
|
||||||
|
if stream == nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
stream.mu.Lock()
|
||||||
|
defer stream.mu.Unlock()
|
||||||
|
if stream.ManualCompaction.Requested {
|
||||||
|
return strings.TrimSpace(stream.ManualCompaction.Instruction), true
|
||||||
|
}
|
||||||
|
return parseManualCompactionDirective(stream.LatestUserText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCursorSummarizeCommand(command *agentv1.SelectedCursorCommand) bool {
|
||||||
|
if command == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if strings.EqualFold(strings.TrimSpace(command.GetName()), "glass-action-summarize") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, ok := parseManualCompactionDirective(command.GetContent())
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
func parseManualCompactionDirective(latestUserText string) (string, bool) {
|
func parseManualCompactionDirective(latestUserText string) (string, bool) {
|
||||||
trimmed := strings.TrimSpace(latestUserText)
|
trimmed := strings.TrimSpace(latestUserText)
|
||||||
|
const directive = "/summarize"
|
||||||
switch {
|
switch {
|
||||||
case trimmed == "/compact":
|
case trimmed == directive:
|
||||||
return "", true
|
return "", true
|
||||||
case strings.HasPrefix(trimmed, "/compact "):
|
case strings.HasPrefix(trimmed, directive+" "):
|
||||||
return strings.TrimSpace(strings.TrimPrefix(trimmed, "/compact")), true
|
return strings.TrimSpace(strings.TrimPrefix(trimmed, directive)), true
|
||||||
default:
|
default:
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package forwarder
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"cursor/gen/agentv1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseManualCompactionDirectiveSupportsCursorSummarize(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
wantInstruction string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{name: "compact removed", text: "/compact", want: false},
|
||||||
|
{name: "compact instruction removed", text: "/compact keep deployment details", want: false},
|
||||||
|
{name: "summarize", text: "/summarize", want: true},
|
||||||
|
{name: "summarize instruction", text: "/summarize keep failing tests", wantInstruction: "keep failing tests", want: true},
|
||||||
|
{name: "surrounding whitespace", text: " /summarize ", want: true},
|
||||||
|
{name: "similar command", text: "/summarized", want: false},
|
||||||
|
{name: "ordinary text", text: "please summarize this file", want: false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
instruction, ok := parseManualCompactionDirective(test.text)
|
||||||
|
if ok != test.want || instruction != test.wantInstruction {
|
||||||
|
t.Fatalf("parseManualCompactionDirective(%q) = (%q, %v), want (%q, %v)", test.text, instruction, ok, test.wantInstruction, test.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseManualCompactionRequestRecognizesCursorSummarizeCommand(t *testing.T) {
|
||||||
|
instruction, ok := parseManualCompactionRequest(&agentv1.UserMessage{
|
||||||
|
SelectedContext: &agentv1.SelectedContext{
|
||||||
|
CursorCommands: []*agentv1.SelectedCursorCommand{
|
||||||
|
{Name: "glass-action-summarize", Content: "/summarize"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if !ok || instruction != "" {
|
||||||
|
t.Fatalf("parseManualCompactionRequest() = (%q, %v), want empty instruction and true", instruction, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseManualCompactionRequestIgnoresSummarizeMetadataWhenUserTextIsPresent(t *testing.T) {
|
||||||
|
instruction, ok := parseManualCompactionRequest(&agentv1.UserMessage{
|
||||||
|
Text: "why does /summarize not work?",
|
||||||
|
SelectedContext: &agentv1.SelectedContext{
|
||||||
|
CursorCommands: []*agentv1.SelectedCursorCommand{
|
||||||
|
{Name: "glass-action-summarize", Content: "/summarize"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if ok || instruction != "" {
|
||||||
|
t.Fatalf("parseManualCompactionRequest() = (%q, %v), want empty instruction and false", instruction, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseManualCompactionRequestIgnoresOrdinaryCursorCommands(t *testing.T) {
|
||||||
|
instruction, ok := parseManualCompactionRequest(&agentv1.UserMessage{
|
||||||
|
Text: "review this implementation",
|
||||||
|
SelectedContext: &agentv1.SelectedContext{
|
||||||
|
CursorCommands: []*agentv1.SelectedCursorCommand{
|
||||||
|
nil,
|
||||||
|
{Name: "review", Content: "Review the implementation."},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if ok || instruction != "" {
|
||||||
|
t.Fatalf("parseManualCompactionRequest() = (%q, %v), want empty instruction and false", instruction, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeInboundIntentMapsRunRequestSummarizeActionToManualCompaction(t *testing.T) {
|
||||||
|
service := &Service{debug: newDebugRecorder("", nil, nil)}
|
||||||
|
intent, err := service.decodeInboundIntent(
|
||||||
|
"summarize-request",
|
||||||
|
newRunRequestMessage(newSummarizeConversationAction()),
|
||||||
|
"run_request",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodeInboundIntent() error = %v", err)
|
||||||
|
}
|
||||||
|
if intent.Kind != "run" || !intent.StartsRun {
|
||||||
|
t.Fatalf("decodeInboundIntent() kind = %q, starts_run = %v, want run and true", intent.Kind, intent.StartsRun)
|
||||||
|
}
|
||||||
|
if intent.UserMessage != nil {
|
||||||
|
t.Fatalf("decodeInboundIntent() user_message = %#v, want nil", intent.UserMessage)
|
||||||
|
}
|
||||||
|
if !intent.ManualCompaction.Requested || intent.ManualCompaction.Instruction != "" {
|
||||||
|
t.Fatalf("decodeInboundIntent() manual_compaction = %#v, want requested with empty instruction", intent.ManualCompaction)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveInboundManualCompactionSupportsStandaloneConversationAction(t *testing.T) {
|
||||||
|
directive := resolveInboundManualCompaction(&agentv1.AgentClientMessage{
|
||||||
|
Message: &agentv1.AgentClientMessage_ConversationAction{
|
||||||
|
ConversationAction: newSummarizeConversationAction(),
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if !directive.Requested || directive.Instruction != "" {
|
||||||
|
t.Fatalf("resolveInboundManualCompaction() = %#v, want requested with empty instruction", directive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveInboundManualCompactionIgnoresOtherStandaloneActions(t *testing.T) {
|
||||||
|
directive := resolveInboundManualCompaction(&agentv1.AgentClientMessage{
|
||||||
|
Message: &agentv1.AgentClientMessage_ConversationAction{
|
||||||
|
ConversationAction: &agentv1.ConversationAction{
|
||||||
|
Action: &agentv1.ConversationAction_CancelAction{CancelAction: &agentv1.CancelAction{}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
if directive.Requested {
|
||||||
|
t.Fatalf("resolveInboundManualCompaction() = %#v, want not requested", directive)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeInboundIntentDoesNotMapOrdinaryRunActionsToManualCompaction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
text string
|
||||||
|
}{
|
||||||
|
{name: "ordinary message", text: "review this implementation"},
|
||||||
|
{name: "compact command removed", text: "/compact"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
service := &Service{debug: newDebugRecorder("", nil, nil)}
|
||||||
|
intent, err := service.decodeInboundIntent(
|
||||||
|
"ordinary-request",
|
||||||
|
newRunRequestMessage(&agentv1.ConversationAction{
|
||||||
|
Action: &agentv1.ConversationAction_UserMessageAction{
|
||||||
|
UserMessageAction: &agentv1.UserMessageAction{
|
||||||
|
UserMessage: &agentv1.UserMessage{Text: test.text},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
"run_request",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decodeInboundIntent() error = %v", err)
|
||||||
|
}
|
||||||
|
if intent.ManualCompaction.Requested {
|
||||||
|
t.Fatalf("decodeInboundIntent() manual_compaction = %#v, want not requested", intent.ManualCompaction)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSummarizeConversationActionStartsRun(t *testing.T) {
|
||||||
|
if !conversationActionStartsRun(newSummarizeConversationAction()) {
|
||||||
|
t.Fatal("conversationActionStartsRun(summarize) = false, want true")
|
||||||
|
}
|
||||||
|
if conversationActionStartsRun(&agentv1.ConversationAction{
|
||||||
|
Action: &agentv1.ConversationAction_CancelAction{CancelAction: &agentv1.CancelAction{}},
|
||||||
|
}) {
|
||||||
|
t.Fatal("conversationActionStartsRun(cancel) = true, want false")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStreamManualCompactionDirectiveUsesStructuredRequest(t *testing.T) {
|
||||||
|
stream := &ActiveStream{
|
||||||
|
LatestUserText: "visible user text",
|
||||||
|
ManualCompaction: manualCompactionDirective{
|
||||||
|
Requested: true,
|
||||||
|
Instruction: "keep decisions",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
instruction, ok := streamManualCompactionDirective(stream)
|
||||||
|
if !ok || instruction != "keep decisions" {
|
||||||
|
t.Fatalf("streamManualCompactionDirective() = (%q, %v), want (%q, true)", instruction, ok, "keep decisions")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRunRequestMessage(action *agentv1.ConversationAction) *agentv1.AgentClientMessage {
|
||||||
|
conversationID := "test-conversation"
|
||||||
|
return &agentv1.AgentClientMessage{
|
||||||
|
Message: &agentv1.AgentClientMessage_RunRequest{
|
||||||
|
RunRequest: &agentv1.AgentRunRequest{
|
||||||
|
ConversationId: &conversationID,
|
||||||
|
Action: action,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSummarizeConversationAction() *agentv1.ConversationAction {
|
||||||
|
return &agentv1.ConversationAction{
|
||||||
|
Action: &agentv1.ConversationAction_SummarizeAction{
|
||||||
|
SummarizeAction: &agentv1.SummarizeAction{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -672,6 +672,7 @@ func (service *Service) decodeInboundIntent(requestID string, message *agentv1.A
|
|||||||
default:
|
default:
|
||||||
return InboundIntent{}, fmt.Errorf("unsupported client message kind: %s", clientKind)
|
return InboundIntent{}, fmt.Errorf("unsupported client message kind: %s", clientKind)
|
||||||
}
|
}
|
||||||
|
intent.ManualCompaction = resolveInboundManualCompaction(message, intent.UserMessage)
|
||||||
return intent, nil
|
return intent, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,6 +752,7 @@ func (service *Service) handleRunIntent(intent InboundIntent) error {
|
|||||||
stream.mu.Lock()
|
stream.mu.Lock()
|
||||||
stream.ThinkingEffort = strings.TrimSpace(intent.ThinkingEffort)
|
stream.ThinkingEffort = strings.TrimSpace(intent.ThinkingEffort)
|
||||||
stream.SubagentModelOverrides = cloneSubagentModelOverrides(intent.SubagentModelOverrides)
|
stream.SubagentModelOverrides = cloneSubagentModelOverrides(intent.SubagentModelOverrides)
|
||||||
|
stream.ManualCompaction = intent.ManualCompaction
|
||||||
stream.PendingProviderAction = providerActionNone
|
stream.PendingProviderAction = providerActionNone
|
||||||
stream.PendingCompaction = nil
|
stream.PendingCompaction = nil
|
||||||
stream.PendingExecs = make(map[string]runtimecore.PendingExec)
|
stream.PendingExecs = make(map[string]runtimecore.PendingExec)
|
||||||
@@ -788,6 +790,7 @@ func (service *Service) handleRunIntent(intent InboundIntent) error {
|
|||||||
"subagent_model_override_count": len(intent.SubagentModelOverrides),
|
"subagent_model_override_count": len(intent.SubagentModelOverrides),
|
||||||
"subagent_model_overrides": subagentModelOverrideSummaries(intent.SubagentModelOverrides),
|
"subagent_model_overrides": subagentModelOverrideSummaries(intent.SubagentModelOverrides),
|
||||||
"latest_user_text": userMessageText(intent.UserMessage),
|
"latest_user_text": userMessageText(intent.UserMessage),
|
||||||
|
"manual_compaction_requested": intent.ManualCompaction.Requested,
|
||||||
})
|
})
|
||||||
if err := service.publishCheckpoint(intent.RequestID, intent.ConversationID); err != nil {
|
if err := service.publishCheckpoint(intent.RequestID, intent.ConversationID); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -2643,6 +2646,38 @@ func conversationActionIsResume(action *agentv1.ConversationAction) bool {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inboundConversationAction(message *agentv1.AgentClientMessage) *agentv1.ConversationAction {
|
||||||
|
if message == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if action := message.GetConversationAction(); action != nil {
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
if runRequest := message.GetRunRequest(); runRequest != nil {
|
||||||
|
return runRequest.GetAction()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func conversationActionIsSummarize(action *agentv1.ConversationAction) bool {
|
||||||
|
if action == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := action.GetAction().(*agentv1.ConversationAction_SummarizeAction)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveInboundManualCompaction(message *agentv1.AgentClientMessage, userMessage *agentv1.UserMessage) manualCompactionDirective {
|
||||||
|
instruction, requested := parseManualCompactionRequest(userMessage)
|
||||||
|
if conversationActionIsSummarize(inboundConversationAction(message)) {
|
||||||
|
requested = true
|
||||||
|
}
|
||||||
|
return manualCompactionDirective{
|
||||||
|
Requested: requested,
|
||||||
|
Instruction: instruction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func conversationActionStartsRun(action *agentv1.ConversationAction) bool {
|
func conversationActionStartsRun(action *agentv1.ConversationAction) bool {
|
||||||
if action == nil {
|
if action == nil {
|
||||||
return false
|
return false
|
||||||
@@ -2650,6 +2685,7 @@ func conversationActionStartsRun(action *agentv1.ConversationAction) bool {
|
|||||||
switch action.GetAction().(type) {
|
switch action.GetAction().(type) {
|
||||||
case *agentv1.ConversationAction_UserMessageAction,
|
case *agentv1.ConversationAction_UserMessageAction,
|
||||||
*agentv1.ConversationAction_ResumeAction,
|
*agentv1.ConversationAction_ResumeAction,
|
||||||
|
*agentv1.ConversationAction_SummarizeAction,
|
||||||
*agentv1.ConversationAction_StartPlanAction,
|
*agentv1.ConversationAction_StartPlanAction,
|
||||||
*agentv1.ConversationAction_ExecutePlanAction:
|
*agentv1.ConversationAction_ExecutePlanAction:
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ type StreamSubscriber struct {
|
|||||||
Signal chan struct{}
|
Signal chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type manualCompactionDirective struct {
|
||||||
|
Requested bool
|
||||||
|
Instruction string
|
||||||
|
}
|
||||||
|
|
||||||
type ActiveStream struct {
|
type ActiveStream struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
|
||||||
@@ -126,6 +131,7 @@ type ActiveStream struct {
|
|||||||
ModelName string
|
ModelName string
|
||||||
Mode agentv1.AgentMode
|
Mode agentv1.AgentMode
|
||||||
LatestUserText string
|
LatestUserText string
|
||||||
|
ManualCompaction manualCompactionDirective
|
||||||
Status StreamStatus
|
Status StreamStatus
|
||||||
ThinkingEffort string
|
ThinkingEffort string
|
||||||
SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection
|
SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection
|
||||||
@@ -407,6 +413,7 @@ type InboundIntent struct {
|
|||||||
HasExplicitMode bool
|
HasExplicitMode bool
|
||||||
ModeSource ModeSource
|
ModeSource ModeSource
|
||||||
StartsRun bool
|
StartsRun bool
|
||||||
|
ManualCompaction manualCompactionDirective
|
||||||
SubagentTypeName string
|
SubagentTypeName string
|
||||||
SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection
|
SubagentModelOverrides map[string]runtimecore.SubagentModelOverrideSelection
|
||||||
ConversationState *agentv1.ConversationStateStructure
|
ConversationState *agentv1.ConversationStateStructure
|
||||||
|
|||||||
Reference in New Issue
Block a user