fix: send final response after message tool use and improve end-of-turn detection
The message tool's sentInRound flag was suppressing the LLM's final response when an intermediate message had been sent on the same channel. On the client side, statusText was cleared on every incoming message, making it impossible to distinguish regular messages from status_end (turn completion signal). Server: remove sentInRound suppression so final response is always sent. Client: only clear statusText on status_end, detect end-of-turn via hasSpoken + statusText==null in both Heartbeat and Message handlers. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
0524dc7b65
commit
bf8b433812
4 changed files with 16 additions and 31 deletions
|
|
@ -51,7 +51,6 @@ class AssistantConnectionImpl(
|
||||||
"status_end" -> _statusText.value = null
|
"status_end" -> _statusText.value = null
|
||||||
"tool_request" -> handleToolRequest(dto.content)
|
"tool_request" -> handleToolRequest(dto.content)
|
||||||
else -> {
|
else -> {
|
||||||
_statusText.value = null
|
|
||||||
_messages.emit(AssistantMessage(content = dto.content, type = dto.type))
|
_messages.emit(AssistantMessage(content = dto.content, type = dto.type))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -190,13 +190,14 @@ class AssistantManager(
|
||||||
|
|
||||||
val statusJob = launch {
|
val statusJob = launch {
|
||||||
connection.statusText.collect { label ->
|
connection.statusText.collect { label ->
|
||||||
if (label != null) heartbeat.trySend(Unit)
|
heartbeat.trySend(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
_state.update { it.copy(phase = VoicePhase.THINKING, statusText = null) }
|
_state.update { it.copy(phase = VoicePhase.THINKING, statusText = null) }
|
||||||
|
|
||||||
|
var hasSpoken = false
|
||||||
while (true) {
|
while (true) {
|
||||||
val result = select<WaitResult> {
|
val result = select<WaitResult> {
|
||||||
speechQueue.onReceive { WaitResult.Message(it) }
|
speechQueue.onReceive { WaitResult.Message(it) }
|
||||||
|
|
@ -206,15 +207,21 @@ class AssistantManager(
|
||||||
|
|
||||||
when (result) {
|
when (result) {
|
||||||
WaitResult.Timeout -> {
|
WaitResult.Timeout -> {
|
||||||
_state.update {
|
if (!hasSpoken) {
|
||||||
it.copy(phase = VoicePhase.ERROR, errorMessage = "Response timed out")
|
_state.update {
|
||||||
|
it.copy(phase = VoicePhase.ERROR, errorMessage = "Response timed out")
|
||||||
|
}
|
||||||
|
delay(2000)
|
||||||
}
|
}
|
||||||
delay(2000)
|
|
||||||
return@coroutineScope
|
return@coroutineScope
|
||||||
}
|
}
|
||||||
WaitResult.Heartbeat -> continue
|
WaitResult.Heartbeat -> {
|
||||||
|
if (hasSpoken && connection.statusText.value == null) return@coroutineScope
|
||||||
|
continue
|
||||||
|
}
|
||||||
is WaitResult.Message -> {
|
is WaitResult.Message -> {
|
||||||
speakAndDrain(result.content, speechQueue)
|
speakAndDrain(result.content, speechQueue)
|
||||||
|
hasSpoken = true
|
||||||
val currentStatus = connection.statusText.value
|
val currentStatus = connection.statusText.value
|
||||||
if (currentStatus == null) return@coroutineScope
|
if (currentStatus == null) return@coroutineScope
|
||||||
_state.update {
|
_state.update {
|
||||||
|
|
@ -231,7 +238,7 @@ class AssistantManager(
|
||||||
private suspend fun speakAndDrain(firstContent: String, speechQueue: Channel<String>) {
|
private suspend fun speakAndDrain(firstContent: String, speechQueue: Channel<String>) {
|
||||||
val chunks = mutableListOf(firstContent)
|
val chunks = mutableListOf(firstContent)
|
||||||
try {
|
try {
|
||||||
_state.update { it.copy(phase = VoicePhase.SPEAKING, responseText = firstContent) }
|
_state.update { it.copy(phase = VoicePhase.SPEAKING, responseText = firstContent, statusText = null) }
|
||||||
ttsWrapper.speak(firstContent)
|
ttsWrapper.speak(firstContent)
|
||||||
while (true) {
|
while (true) {
|
||||||
val next = speechQueue.tryReceive().getOrNull() ?: break
|
val next = speechQueue.tryReceive().getOrNull() ?: break
|
||||||
|
|
|
||||||
|
|
@ -296,17 +296,9 @@ func (al *AgentLoop) Run(ctx context.Context) error {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if response != "" {
|
if response != "" {
|
||||||
alreadySent := false
|
al.bus.PublishOutbound(bus.OutboundMessage{
|
||||||
if tool, ok := al.tools.Get("message"); ok {
|
Channel: m.Channel, ChatID: m.ChatID, Content: response,
|
||||||
if mt, ok := tool.(*tools.MessageTool); ok {
|
})
|
||||||
alreadySent = mt.HasSentInRound()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !alreadySent {
|
|
||||||
al.bus.PublishOutbound(bus.OutboundMessage{
|
|
||||||
Channel: m.Channel, ChatID: m.ChatID, Content: response,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}(msg, sessionKey)
|
}(msg, sessionKey)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ type MessageTool struct {
|
||||||
sendCallback SendCallback
|
sendCallback SendCallback
|
||||||
defaultChannel string
|
defaultChannel string
|
||||||
defaultChatID string
|
defaultChatID string
|
||||||
sentInRound bool // Tracks whether a message was sent in the current processing round
|
|
||||||
enabledChannels []string
|
enabledChannels []string
|
||||||
stateResolver StateResolver
|
stateResolver StateResolver
|
||||||
}
|
}
|
||||||
|
|
@ -74,12 +73,6 @@ func (t *MessageTool) SetStateResolver(sr StateResolver) {
|
||||||
func (t *MessageTool) SetContext(channel, chatID string) {
|
func (t *MessageTool) SetContext(channel, chatID string) {
|
||||||
t.defaultChannel = channel
|
t.defaultChannel = channel
|
||||||
t.defaultChatID = chatID
|
t.defaultChatID = chatID
|
||||||
t.sentInRound = false // Reset send tracking for new processing round
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasSentInRound returns true if the message tool sent a message during the current round.
|
|
||||||
func (t *MessageTool) HasSentInRound() bool {
|
|
||||||
return t.sentInRound
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *MessageTool) SetSendCallback(callback SendCallback) {
|
func (t *MessageTool) SetSendCallback(callback SendCallback) {
|
||||||
|
|
@ -146,12 +139,6 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only mark as "sent in round" when the message went to the originating channel.
|
|
||||||
// Cross-channel sends (e.g. WS→Discord) must NOT suppress the response
|
|
||||||
// back to the sender's channel.
|
|
||||||
if channel == t.defaultChannel {
|
|
||||||
t.sentInRound = true
|
|
||||||
}
|
|
||||||
// Silent: user already received the message directly
|
// Silent: user already received the message directly
|
||||||
return &ToolResult{
|
return &ToolResult{
|
||||||
ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID),
|
ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID),
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue