Prevent duplicate live reasoning

This commit is contained in:
Kaede Akino committed 2026-08-27 11:53:57 +08:00
1 parent ffca27dace
commit e3ee3fc62e
2 files changed
+72

No files matched your search

@@ -1002,6 +1002,10 @@ final class ConversationModel: Identifiable {
})
let reasoningTitles = Dictionary(uniqueKeysWithValues: processMessages.compactMap {
message -> (UUID, String)? in
// The live reasoning row owns this text until its provider
// turn ends. A tool batch from the same response may reuse it
// afterwards, but must never render a duplicate alongside it.
guard message.id != activeReasoningMessageID else { return nil }
let reasoning = message.reasoning.map(\.text).joined(separator: "\n\n")
let title = AgentToolPresentation.reasoningTitle(from: reasoning)
return title.isEmpty ? nil : (message.id, title)
+68
View File
@@ -718,6 +718,74 @@ struct ChatComposerStateTests {
#expect(terminal.reasoningTitle == nil)
}
@Test
func liveToolBatchDoesNotDuplicateCurrentlyVisibleReasoning() throws {
let conversation = ConversationModel()
conversation.appendUserTurn("inspect both")
let runID = UUID()
let start = Date()
let reasoning = AgentReasoningBlock(text: "Compare both hosts.")
let source = AgentTranscriptMessage(
role: .assistant, reasoning: [reasoning],
toolCalls: ["first", "second"].map {
AgentToolCall(id: $0, name: "lookup", arguments: .object([:]))
},
createdAt: start.addingTimeInterval(1)
)
conversation.handle(.runStarted(AgentRunSummary(
id: runID, state: .running, startedAt: start
)))
conversation.handle(.messageStarted(source))
conversation.handle(.reasoningDelta(
messageID: source.id, text: reasoning.text
))
conversation.handle(.messageFinished(source))
let descriptor = AgentToolDescriptor(
name: "lookup", summary: "lookup", inputSchema: .object([:]),
target: .local, safety: .locallyReadOnly
)
for (offset, call) in source.toolCalls.enumerated() {
conversation.handle(.toolProposed(AgentToolInvocation(
runID: runID, call: call, sourceMessageID: source.id,
createdAt: start.addingTimeInterval(Double(offset + 2))
), descriptor))
}
let overlappingGroup = try #require(conversation.workGroups.first)
#expect(overlappingGroup.items.contains { item in
guard case .reasoning = item else { return false }
return true
})
guard case .tools(let overlappingBatch) = try #require(
overlappingGroup.items.first(where: {
if case .tools = $0 { return true }
return false
})
) else {
Issue.record("Expected a live tool batch")
return
}
#expect(overlappingBatch.reasoningTitle == nil)
conversation.handle(.turnFinished(index: 0, stopReason: .toolCalls))
let settledGroup = try #require(conversation.workGroups.first)
#expect(!settledGroup.items.contains { item in
guard case .reasoning = item else { return false }
return true
})
guard case .tools(let settledBatch) = try #require(
settledGroup.items.first(where: {
if case .tools = $0 { return true }
return false
})
) else {
Issue.record("Expected the live tool batch to remain")
return
}
#expect(settledBatch.reasoningTitle == reasoning.text)
}
@Test
func directAnswerDoesNotCreateAWorkGroup() {
let conversation = ConversationModel()