mirror of
https://github.com/memohai/Memoh.git
synced 2026-04-27 07:16:19 +09:00
fix(agent): reject send tool when targeting the same conversation
Pass replyTarget through the full pipeline (ChatRequest → gateway identity → agent headers → MCP session) so the send tool can detect when the destination matches the current conversation and return an error guiding the agent to reply directly instead.
This commit is contained in:
@@ -31,6 +31,7 @@ export const IdentityContextModel = z.object({
|
|||||||
channelIdentityId: z.string().min(1, 'Channel identity ID is required'),
|
channelIdentityId: z.string().min(1, 'Channel identity ID is required'),
|
||||||
displayName: z.string().min(1, 'Display name is required'),
|
displayName: z.string().min(1, 'Display name is required'),
|
||||||
currentPlatform: z.string().optional(),
|
currentPlatform: z.string().optional(),
|
||||||
|
replyTarget: z.string().optional(),
|
||||||
conversationType: z.string().optional(),
|
conversationType: z.string().optional(),
|
||||||
sessionToken: z.string().optional(),
|
sessionToken: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ func (p *ChannelInboundProcessor) HandleInbound(ctx context.Context, cfg channel
|
|||||||
RouteID: resolved.RouteID,
|
RouteID: resolved.RouteID,
|
||||||
ChatToken: chatToken,
|
ChatToken: chatToken,
|
||||||
ExternalMessageID: sourceMessageID,
|
ExternalMessageID: sourceMessageID,
|
||||||
|
ReplyTarget: target,
|
||||||
ConversationType: msg.Conversation.Type,
|
ConversationType: msg.Conversation.Type,
|
||||||
ConversationName: msg.Conversation.Name,
|
ConversationName: msg.Conversation.Name,
|
||||||
Query: text,
|
Query: text,
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ type gatewayIdentity struct {
|
|||||||
ChannelIdentityID string `json:"channelIdentityId"`
|
ChannelIdentityID string `json:"channelIdentityId"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
CurrentPlatform string `json:"currentPlatform,omitempty"`
|
CurrentPlatform string `json:"currentPlatform,omitempty"`
|
||||||
|
ReplyTarget string `json:"replyTarget,omitempty"`
|
||||||
ConversationType string `json:"conversationType,omitempty"`
|
ConversationType string `json:"conversationType,omitempty"`
|
||||||
SessionToken string `json:"sessionToken,omitempty"` //nolint:gosec // intentional: session token forwarded to agent gateway for channel reply routing
|
SessionToken string `json:"sessionToken,omitempty"` //nolint:gosec // intentional: session token forwarded to agent gateway for channel reply routing
|
||||||
}
|
}
|
||||||
@@ -470,6 +471,7 @@ func (r *Resolver) resolve(ctx context.Context, req conversation.ChatRequest) (r
|
|||||||
ChannelIdentityID: strings.TrimSpace(req.SourceChannelIdentityID),
|
ChannelIdentityID: strings.TrimSpace(req.SourceChannelIdentityID),
|
||||||
DisplayName: displayName,
|
DisplayName: displayName,
|
||||||
CurrentPlatform: req.CurrentChannel,
|
CurrentPlatform: req.CurrentChannel,
|
||||||
|
ReplyTarget: strings.TrimSpace(req.ReplyTarget),
|
||||||
ConversationType: strings.TrimSpace(req.ConversationType),
|
ConversationType: strings.TrimSpace(req.ConversationType),
|
||||||
SessionToken: req.ChatToken,
|
SessionToken: req.ChatToken,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ type ChatRequest struct {
|
|||||||
RouteID string `json:"-"`
|
RouteID string `json:"-"`
|
||||||
ChatToken string `json:"-"`
|
ChatToken string `json:"-"`
|
||||||
ExternalMessageID string `json:"-"`
|
ExternalMessageID string `json:"-"`
|
||||||
|
ReplyTarget string `json:"-"`
|
||||||
ConversationType string `json:"-"`
|
ConversationType string `json:"-"`
|
||||||
ConversationName string `json:"-"`
|
ConversationName string `json:"-"`
|
||||||
UserMessagePersisted bool `json:"-"`
|
UserMessagePersisted bool `json:"-"`
|
||||||
|
|||||||
@@ -227,6 +227,16 @@ func (p *Executor) callSend(ctx context.Context, session mcpgw.ToolSessionContex
|
|||||||
return mcpgw.BuildToolErrorResult("target is required"), nil
|
return mcpgw.BuildToolErrorResult("target is required"), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reject send when the destination matches the current conversation.
|
||||||
|
if strings.EqualFold(channelType.String(), strings.TrimSpace(session.CurrentPlatform)) &&
|
||||||
|
target == strings.TrimSpace(session.ReplyTarget) {
|
||||||
|
return mcpgw.BuildToolErrorResult(
|
||||||
|
"You are trying to send a message to the SAME conversation you are already in. " +
|
||||||
|
"Do NOT use the send tool for this. Instead, write your reply as plain text directly. " +
|
||||||
|
"To include files, use the <attachments> block in your response (e.g. <attachments>[{\"type\":\"image\",\"path\":\"/data/media/file.jpg\"}]</attachments>).",
|
||||||
|
), nil
|
||||||
|
}
|
||||||
|
|
||||||
sendReq := channel.SendRequest{
|
sendReq := channel.SendRequest{
|
||||||
Target: target,
|
Target: target,
|
||||||
Message: outboundMessage,
|
Message: outboundMessage,
|
||||||
|
|||||||
@@ -252,7 +252,7 @@ func TestExecutor_CallTool_SendError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecutor_CallTool_Success(t *testing.T) {
|
func TestExecutor_CallTool_SameRouteRejected(t *testing.T) {
|
||||||
sender := &fakeSender{}
|
sender := &fakeSender{}
|
||||||
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||||
@@ -263,6 +263,23 @@ func TestExecutor_CallTool_Success(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if isErr, _ := result["isError"].(bool); !isErr {
|
||||||
|
t.Error("expected error when sending to the same route as current session")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecutor_CallTool_Success(t *testing.T) {
|
||||||
|
sender := &fakeSender{}
|
||||||
|
resolver := &fakeResolver{ct: channel.ChannelType("feishu")}
|
||||||
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||||
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "feishu", ReplyTarget: "chat1"}
|
||||||
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||||
|
"target": "chat2",
|
||||||
|
"text": "hello",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
if err := mcpgw.PayloadError(result); err != nil {
|
if err := mcpgw.PayloadError(result); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -284,6 +301,7 @@ func TestExecutor_CallTool_ReplyTo(t *testing.T) {
|
|||||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||||
|
"target": "456",
|
||||||
"text": "reply text",
|
"text": "reply text",
|
||||||
"reply_to": "msg-789",
|
"reply_to": "msg-789",
|
||||||
})
|
})
|
||||||
@@ -307,7 +325,8 @@ func TestExecutor_CallTool_NoReplyTo(t *testing.T) {
|
|||||||
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
exec := NewExecutor(nil, sender, nil, resolver, nil)
|
||||||
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
session := mcpgw.ToolSessionContext{BotID: "bot1", CurrentPlatform: "telegram", ReplyTarget: "123"}
|
||||||
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
result, err := exec.CallTool(context.Background(), session, toolSend, map[string]any{
|
||||||
"text": "no reply",
|
"target": "456",
|
||||||
|
"text": "no reply",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export interface IdentityContext {
|
|||||||
channelIdentityId: string
|
channelIdentityId: string
|
||||||
displayName: string
|
displayName: string
|
||||||
currentPlatform?: string
|
currentPlatform?: string
|
||||||
|
replyTarget?: string
|
||||||
conversationType?: string
|
conversationType?: string
|
||||||
sessionToken?: string
|
sessionToken?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ export const buildIdentityHeaders = (identity: IdentityContext, auth: AgentAuthC
|
|||||||
if (identity.currentPlatform) {
|
if (identity.currentPlatform) {
|
||||||
headers['X-Memoh-Current-Platform'] = identity.currentPlatform
|
headers['X-Memoh-Current-Platform'] = identity.currentPlatform
|
||||||
}
|
}
|
||||||
|
if (identity.replyTarget) {
|
||||||
|
headers['X-Memoh-Reply-Target'] = identity.replyTarget
|
||||||
|
}
|
||||||
if (options?.isSubagent) {
|
if (options?.isSubagent) {
|
||||||
headers['X-Memoh-Is-Subagent'] = 'true'
|
headers['X-Memoh-Is-Subagent'] = 'true'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user