fix(telegram): clean up subagent tool feedback
This commit is contained in:
parent
5292ddf325
commit
fdbca6d56d
6 changed files with 131 additions and 0 deletions
|
|
@ -49,3 +49,12 @@ func (a *channelManagerAdapter) DismissToolFeedback(
|
||||||
) {
|
) {
|
||||||
a.inner.DismissToolFeedback(ctx, channel, chatID, outboundCtx)
|
a.inner.DismissToolFeedback(ctx, channel, chatID, outboundCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *channelManagerAdapter) DismissToolFeedbackForSession(
|
||||||
|
ctx context.Context,
|
||||||
|
channel, chatID string,
|
||||||
|
outboundCtx *bus.InboundContext,
|
||||||
|
sessionKey string,
|
||||||
|
) {
|
||||||
|
a.inner.DismissToolFeedbackForSession(ctx, channel, chatID, outboundCtx, sessionKey)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,16 @@ type ChannelManager interface {
|
||||||
// outboundCtx carries topic/thread info needed for channels that use
|
// outboundCtx carries topic/thread info needed for channels that use
|
||||||
// scoped tracker keys (e.g., Telegram forum topics); may be nil.
|
// scoped tracker keys (e.g., Telegram forum topics); may be nil.
|
||||||
DismissToolFeedback(ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext)
|
DismissToolFeedback(ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext)
|
||||||
|
|
||||||
|
// DismissToolFeedbackForSession clears a session-scoped tool feedback
|
||||||
|
// message. This is used for background sub-turns whose progress messages
|
||||||
|
// are visible in the originating chat, but whose final result is delivered
|
||||||
|
// asynchronously through the parent turn instead of as a direct channel
|
||||||
|
// response.
|
||||||
|
DismissToolFeedbackForSession(
|
||||||
|
ctx context.Context,
|
||||||
|
channel, chatID string,
|
||||||
|
outboundCtx *bus.InboundContext,
|
||||||
|
sessionKey string,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -447,6 +447,17 @@ func spawnSubTurn(
|
||||||
|
|
||||||
// Result Delivery Strategy (Async vs Sync)
|
// Result Delivery Strategy (Async vs Sync)
|
||||||
if cfg.Async {
|
if cfg.Async {
|
||||||
|
if al != nil && al.channelManager != nil && childTS.channel != "" {
|
||||||
|
dismissCtx, dismissCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
al.channelManager.DismissToolFeedbackForSession(
|
||||||
|
dismissCtx,
|
||||||
|
childTS.channel,
|
||||||
|
childTS.chatID,
|
||||||
|
childTS.opts.InboundContext,
|
||||||
|
childID,
|
||||||
|
)
|
||||||
|
dismissCancel()
|
||||||
|
}
|
||||||
deliverSubTurnResult(al, parentTS, childID, result)
|
deliverSubTurnResult(al, parentTS, childID, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,32 @@ func dismissTrackedToolFeedbackMessage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func dismissTrackedToolFeedbackMessageForSession(
|
||||||
|
ctx context.Context,
|
||||||
|
ch Channel,
|
||||||
|
chatID string,
|
||||||
|
outboundCtx *bus.InboundContext,
|
||||||
|
sessionKey string,
|
||||||
|
) {
|
||||||
|
sessionKey = strings.TrimSpace(sessionKey)
|
||||||
|
if sessionKey == "" {
|
||||||
|
dismissTrackedToolFeedbackMessage(ctx, ch, chatID, outboundCtx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trackedChatID := resolveOutboundChatID(ch, chatID, outboundCtx)
|
||||||
|
if trackedChatID == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trackedChatID += "#session:" + sessionKey
|
||||||
|
if cleaner, ok := ch.(toolFeedbackMessageCleaner); ok {
|
||||||
|
cleaner.DismissToolFeedbackMessage(ctx, trackedChatID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tracker, ok := ch.(toolFeedbackMessageTracker); ok {
|
||||||
|
tracker.ClearToolFeedbackMessage(trackedChatID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func clearTrackedToolFeedbackMessage(
|
func clearTrackedToolFeedbackMessage(
|
||||||
ch Channel,
|
ch Channel,
|
||||||
chatID string,
|
chatID string,
|
||||||
|
|
@ -235,6 +261,16 @@ func (m *Manager) DismissToolFeedback(
|
||||||
dismissTrackedToolFeedbackMessage(ctx, ch, chatID, outboundCtx)
|
dismissTrackedToolFeedbackMessage(ctx, ch, chatID, outboundCtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Manager) DismissToolFeedbackForSession(
|
||||||
|
ctx context.Context, channelName, chatID string, outboundCtx *bus.InboundContext, sessionKey string,
|
||||||
|
) {
|
||||||
|
ch, ok := m.GetChannel(channelName)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dismissTrackedToolFeedbackMessageForSession(ctx, ch, chatID, outboundCtx, sessionKey)
|
||||||
|
}
|
||||||
|
|
||||||
func prepareToolFeedbackMessageContent(ch Channel, content string) string {
|
func prepareToolFeedbackMessageContent(ch Channel, content string) string {
|
||||||
prepared := strings.TrimSpace(content)
|
prepared := strings.TrimSpace(content)
|
||||||
if prepared == "" {
|
if prepared == "" {
|
||||||
|
|
|
||||||
|
|
@ -468,6 +468,7 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag
|
||||||
|
|
||||||
// DeleteMessage implements channels.MessageDeleter.
|
// DeleteMessage implements channels.MessageDeleter.
|
||||||
func (c *TelegramChannel) DeleteMessage(ctx context.Context, chatID string, messageID string) error {
|
func (c *TelegramChannel) DeleteMessage(ctx context.Context, chatID string, messageID string) error {
|
||||||
|
chatID = telegramToolFeedbackDeliveryChatID(chatID)
|
||||||
cid, _, err := parseTelegramChatID(chatID)
|
cid, _, err := parseTelegramChatID(chatID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
||||||
|
|
@ -383,6 +383,7 @@ func TestSend_FinalReplyDoesNotFinalizeDifferentSessionToolFeedback(t *testing.T
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
ch := newTestChannel(t, caller)
|
ch := newTestChannel(t, caller)
|
||||||
|
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
||||||
|
|
||||||
baseCtx := bus.InboundContext{
|
baseCtx := bus.InboundContext{
|
||||||
Channel: "telegram",
|
Channel: "telegram",
|
||||||
|
|
@ -421,6 +422,67 @@ func TestSend_FinalReplyDoesNotFinalizeDifferentSessionToolFeedback(t *testing.T
|
||||||
assert.Equal(t, "1", msgID)
|
assert.Equal(t, "1", msgID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSend_SessionScopedToolFeedbackUpdatesExistingTelegramMessage(t *testing.T) {
|
||||||
|
nextMessageID := 0
|
||||||
|
caller := &stubCaller{
|
||||||
|
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
|
||||||
|
nextMessageID++
|
||||||
|
return successResponseWithMessageID(t, nextMessageID), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ch := newTestChannel(t, caller)
|
||||||
|
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
||||||
|
|
||||||
|
baseCtx := bus.InboundContext{
|
||||||
|
Channel: "telegram",
|
||||||
|
ChatID: "-1001234567890",
|
||||||
|
TopicID: "42",
|
||||||
|
Raw: map[string]string{
|
||||||
|
"message_kind": "tool_feedback",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
|
ChatID: "-1001234567890",
|
||||||
|
SessionKey: "subturn-1",
|
||||||
|
Content: "Working...\n• tool: `read_file`",
|
||||||
|
Context: baseCtx,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ids, err := ch.Send(context.Background(), bus.OutboundMessage{
|
||||||
|
ChatID: "-1001234567890",
|
||||||
|
SessionKey: "subturn-1",
|
||||||
|
Content: "Working...\n• tool: `read_file`\n• tool: `mcp_gpt_researcher_deep_research`",
|
||||||
|
Context: baseCtx,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, []string{"1"}, ids)
|
||||||
|
require.Len(t, caller.calls, 2)
|
||||||
|
assert.Contains(t, caller.calls[1].URL, "editMessageText")
|
||||||
|
assert.NotContains(t, caller.calls[1].URL, "%23session")
|
||||||
|
assert.NotContains(t, caller.calls[1].URL, "#session")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDismissToolFeedbackMessage_SessionScopedKeyDeletesTelegramMessage(t *testing.T) {
|
||||||
|
caller := &stubCaller{
|
||||||
|
callFn: func(ctx context.Context, url string, data *ta.RequestData) (*ta.Response, error) {
|
||||||
|
return successResponse(t), nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ch := newTestChannel(t, caller)
|
||||||
|
ch.RecordToolFeedbackMessage("-1001234567890/42#session:subturn-1", "7", "Working...\n• tool: `read_file`")
|
||||||
|
|
||||||
|
ch.DismissToolFeedbackMessage(context.Background(), "-1001234567890/42#session:subturn-1")
|
||||||
|
|
||||||
|
require.Len(t, caller.calls, 1)
|
||||||
|
assert.Contains(t, caller.calls[0].URL, "deleteMessage")
|
||||||
|
assert.NotContains(t, caller.calls[0].URL, "%23session")
|
||||||
|
assert.NotContains(t, caller.calls[0].URL, "#session")
|
||||||
|
_, ok := ch.currentToolFeedbackMessage("-1001234567890/42#session:subturn-1")
|
||||||
|
assert.False(t, ok)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T) {
|
func TestFinalizeTrackedToolFeedbackMessage_StopsTrackingBeforeEdit(t *testing.T) {
|
||||||
ch := newTestChannel(t, &stubCaller{
|
ch := newTestChannel(t, &stubCaller{
|
||||||
callFn: func(context.Context, string, *ta.RequestData) (*ta.Response, error) {
|
callFn: func(context.Context, string, *ta.RequestData) (*ta.Response, error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue