Prevent duplicate live reasoning
This commit is contained in:
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user