Merge pull request #20 from dj-oyu/fix/finalize-draft-on-completion
fix: finalize draft as permanent message on task completion
This commit is contained in:
commit
94c4aedf87
3 changed files with 55 additions and 16 deletions
|
|
@ -54,6 +54,7 @@ type activeTask struct {
|
|||
projectDir string // detected from exec cd target (authoritative)
|
||||
fileCommonDir string // LCP of file paths relative to workspace (fallback)
|
||||
streamedChunks bool // true after onChunk fires at least once
|
||||
messageContent string // last content sent by the message tool (for inclusion in completion)
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
|
|
@ -1063,39 +1064,52 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
if opts.TaskID != "" {
|
||||
elapsed := time.Since(task.StartedAt)
|
||||
completionMsg := fmt.Sprintf("\u2705 Task completed (%.1fs)", elapsed.Seconds())
|
||||
if finalContent != "" && finalContent != defaultResponse && finalContent != "HEARTBEAT_OK" {
|
||||
// Keep completion + response in one bubble if short enough (4096 = Telegram limit).
|
||||
// If too long, edit the status bubble with the header, then send the
|
||||
// full response as a regular message — the channel worker's SplitMessage
|
||||
// will automatically chunk it for channels with MaxMessageLength.
|
||||
combined := completionMsg + "\n\n" + finalContent
|
||||
if len([]rune(combined)) <= 4096 {
|
||||
completionMsg = combined
|
||||
} else {
|
||||
doneCtx, doneCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_ = al.bus.PublishOutbound(doneCtx, bus.OutboundMessage{
|
||||
Channel: opts.Channel,
|
||||
ChatID: opts.ChatID,
|
||||
Content: completionMsg,
|
||||
IsTaskStatus: true,
|
||||
TaskID: opts.TaskID,
|
||||
})
|
||||
_ = al.bus.PublishOutbound(doneCtx, bus.OutboundMessage{
|
||||
Channel: opts.Channel,
|
||||
ChatID: opts.ChatID,
|
||||
Content: finalContent,
|
||||
})
|
||||
doneCancel()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// No finalContent — fall back to task.Result or task.Description
|
||||
|
||||
// Determine the best content to show in the completion bubble.
|
||||
// Priority: message tool content > finalContent > task.Result
|
||||
task.mu.Lock()
|
||||
msgContent := task.messageContent
|
||||
task.mu.Unlock()
|
||||
|
||||
var resultContent string
|
||||
switch {
|
||||
case msgContent != "":
|
||||
// The message tool already sent this to the user via the
|
||||
// task bubble; re-include it so the completion doesn't erase it.
|
||||
resultContent = msgContent
|
||||
case finalContent != "" && finalContent != defaultResponse && finalContent != "HEARTBEAT_OK":
|
||||
resultContent = finalContent
|
||||
default:
|
||||
summary := task.Result
|
||||
if summary == "" {
|
||||
summary = task.Description
|
||||
}
|
||||
if summary != "" {
|
||||
completionMsg += "\n" + summary
|
||||
resultContent = summary
|
||||
}
|
||||
|
||||
if resultContent != "" {
|
||||
combined := completionMsg + "\n\n" + resultContent
|
||||
if len([]rune(combined)) <= 4096 {
|
||||
completionMsg = combined
|
||||
} else {
|
||||
// Too long for one bubble: send header as task status,
|
||||
// body as regular message (auto-split by SplitMessage).
|
||||
doneCtx, doneCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
_ = al.bus.PublishOutbound(doneCtx, bus.OutboundMessage{
|
||||
Channel: opts.Channel,
|
||||
ChatID: opts.ChatID,
|
||||
Content: completionMsg,
|
||||
IsTaskStatus: true,
|
||||
TaskID: opts.TaskID,
|
||||
Final: true,
|
||||
})
|
||||
_ = al.bus.PublishOutbound(doneCtx, bus.OutboundMessage{
|
||||
Channel: opts.Channel,
|
||||
ChatID: opts.ChatID,
|
||||
Content: resultContent,
|
||||
})
|
||||
doneCancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
doneCtx, doneCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
|
|
@ -1105,6 +1119,7 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
Content: completionMsg,
|
||||
IsTaskStatus: true,
|
||||
TaskID: opts.TaskID,
|
||||
Final: true,
|
||||
})
|
||||
doneCancel()
|
||||
}
|
||||
|
|
@ -1135,6 +1150,13 @@ func (al *AgentLoop) runAgentLoop(ctx context.Context, agent *AgentInstance, opt
|
|||
if mt, ok := tool.(*tools.MessageTool); ok {
|
||||
taskID := opts.TaskID
|
||||
mt.SetSendCallback(func(channel, chatID, content string) error {
|
||||
// Capture the message tool's content so the completion
|
||||
// defer can include it instead of losing it to an overwrite.
|
||||
if task != nil {
|
||||
task.mu.Lock()
|
||||
task.messageContent = content
|
||||
task.mu.Unlock()
|
||||
}
|
||||
pubCtx, pubCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer pubCancel()
|
||||
return al.bus.PublishOutbound(pubCtx, bus.OutboundMessage{
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ type OutboundMessage struct {
|
|||
IsStatus bool `json:"is_status,omitempty"`
|
||||
IsTaskStatus bool `json:"is_task_status,omitempty"`
|
||||
TaskID string `json:"task_id,omitempty"`
|
||||
Final bool `json:"final,omitempty"` // Finalize: send as permanent message, not draft
|
||||
SkipPlaceholder bool `json:"skip_placeholder,omitempty"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -577,6 +577,22 @@ func (m *Manager) handleTaskStatusSend(ctx context.Context, name string, w *chan
|
|||
|
||||
taskKey := msg.TaskID
|
||||
|
||||
// Final message: send as permanent (non-draft) message so it persists.
|
||||
// Drafts are ephemeral and disappear after a short time; the completion
|
||||
// message must survive. Clear the draft tracking and send via SendWithID
|
||||
// or regular Send, which creates a permanent Telegram message.
|
||||
if msg.Final {
|
||||
m.taskMsgIDs.Delete(taskKey)
|
||||
m.statusEditTimes.Delete(taskKey)
|
||||
if sender, ok := w.ch.(MessageSenderWithID); ok {
|
||||
if msgID, err := sender.SendWithID(ctx, msg.ChatID, msg.Content); err == nil && msgID != "" {
|
||||
return
|
||||
}
|
||||
}
|
||||
_ = w.ch.Send(ctx, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// 0. Draft-based streaming (preferred for supported channels)
|
||||
if drafter, ok := w.ch.(DraftSender); ok && taskKey != "" {
|
||||
var did int
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue