ios: hide raw tool entries in Agent transcript
TestFlight / Build and upload (push) Successful in 1m59s

This commit is contained in:
2026-10-14 15:40:37 -07:00
parent 56279fba7f
commit 4cbab487d0
5 changed files with 203 additions and 87 deletions
@@ -97,6 +97,161 @@ private func eventually(_ predicate: () -> Bool) async -> Bool {
}
struct AgentTests {
@Test @MainActor func mixedLoadedHistoryFiltersOnlyToolRows() async throws {
var user = agentMessage("user", entry: "1", role: .user)
user.content = "Explain the tool_call and tool result."
let call = agentMessage("call", entry: "2", role: .tool, info: ["status": .string("initiated")])
var reply = agentMessage("reply", entry: "3")
reply.content = "The search tool failed; here is what you can do."
// Role-only results still need hiding even without tool-call metadata.
var result = agentMessage("result", entry: "4", role: .tool)
result.metadata = nil
let markedCall = agentMessage("marked-call", entry: "5", info: ["kind": .string("tool_call")])
var plainReply = agentMessage("plain-reply", entry: "6")
plainReply.metadata = nil
let failure = agentMessage("failure", entry: "7", info: ["status": .string("failed"), "error": .string("Please try again")])
let terminal = agentMessage("terminal", entry: "8", role: .tool, info: ["status": .string("failed"), "error": .string("Raw tool failure"), "resultPreview": .string("Raw result")])
let history = [user, call, reply, result, markedCall, plainReply, failure, terminal].enumerated().map { index, message in
var message = message
message.createdAt.addTimeInterval(Double(index))
return message
}
let client = TestAgentClient(pages: ["0": agentPage(Array(history.dropLast()), cursor: "7", more: true), "7": agentPage([history[7]], cursor: "8")])
let room = SybilAgentRoom(); room.configure(client: client, storage: MemoryOutbox().storage)
defer { room.stop() }
#expect(await eventually { room.connection == "Connected" })
#expect(await client.requestedCursors() == ["0", "7"])
#expect(await client.connectedAfters() == ["8"])
let transcript = AgentTranscript(messages: room.messages)
#expect(transcript.displayed == [history[0], history[2], history[5], history[6]])
#expect(room.snapshot.room.messages == history)
#expect(room.messages.count == 8)
#expect(room.snapshot.room.cursor == "8")
#expect(room.snapshot.room.readCursor == "0")
#expect(room.unreadCount == 2)
#expect(room.snapshot.entranceIDs.isEmpty)
// Shared regular-chat tool metadata remains available and unchanged.
#expect(terminal.isToolCallLog)
#expect(terminal.toolCallMetadata?.resultPreview == "Raw result")
}
@Test func transcriptWindowsAndAnchorsUseVisibleRows() {
var raw: [Message] = [], visible: [Message] = []
for index in 1...160 {
var message = agentMessage("reply-\(index)", entry: "\(index * 2)")
message.createdAt.addTimeInterval(Double(index * 2) * 86_400)
var tool = agentMessage("tool-\(index)", entry: "\(index * 2 + 1)", role: .tool)
// Tools on separate dates must not introduce their own separators.
tool.createdAt = message.createdAt.addingTimeInterval(86_400)
raw += [message, tool]; visible.append(message)
}
let initial = AgentTranscript(messages: raw)
#expect(initial.startIndex == 60)
#expect(initial.displayed == Array(visible.suffix(100)))
let anchorID = initial.displayed.first?.id
let hidden = agentMessage("late-tool", entry: "400", role: .tool)
let updated = AgentTranscript(messages: raw + [hidden], startID: anchorID)
#expect(updated.displayed == initial.displayed)
let earlierID = initial.messages[max(0, initial.startIndex - 100)].id
let expanded = AgentTranscript(messages: raw, startID: earlierID, limit: 200)
#expect(expanded.displayed == visible)
#expect(expanded.displayed.firstIndex(where: { $0.id == anchorID }) == 60)
let hiddenAnchor = AgentTranscript(messages: raw, startID: "tool-160")
#expect(hiddenAnchor.displayed == initial.displayed)
let onlyTools = AgentTranscript(messages: raw.filter { $0.role == .tool })
#expect(onlyTools.messages.isEmpty)
#expect(onlyTools.displayed.isEmpty)
#expect(onlyTools.startIndex == 0)
}
@Test @MainActor func liveToolUpsertsPreserveRawHistoryCursorsAndActivity() async throws {
let user = agentMessage("user", entry: "1", role: .user)
let reply = agentMessage("reply", entry: "2")
let client = TestAgentClient(pages: ["0": agentPage([user, reply], cursor: "2")])
let room = SybilAgentRoom(); room.configure(client: client, storage: MemoryOutbox().storage)
defer { room.stop() }
#expect(await eventually { room.connection == "Connected" })
let queued = agentMessage("queued", entry: "0", role: .user, info: ["status": .string("queued")])
var activity = AgentActivity()
activity.isThinking = true; activity.backgroundWorkers = 2; activity.queued = 1
activity.queuedMessages = [queued]
activity.wait = .init(due: 1_800_000_000_000, reason: "Waiting for background work")
activity.error = "A worker needs attention"
try room.receive(.activity("root", activity)); room.flush()
#expect(await eventually { room.snapshot.isTyping })
let visible = AgentTranscript(messages: room.messages).displayed
#expect(Set(visible.map(\.id)) == ["user", "reply", "queued"])
let start = agentMessage("tool", entry: "3", role: .tool, info: ["status": .string("initiated"), "toolName": .string("search"), "args": .object(["query": .string("fixture")]), "startedAt": .string("start")])
let completed = agentMessage("tool", entry: "4", role: .tool, info: ["status": .string("completed"), "resultPreview": .string("Raw preview")])
let result = agentMessage("tool", entry: "5", role: .tool, info: ["status": .string("completed"), "result": .object(["matches": .number(3)])])
for (message, cursor) in [(start, "3"), (completed, "4"), (result, "5"), (start, "3")] {
try room.receive(.messages(agentPage([message], cursor: cursor))); room.flush()
#expect(AgentTranscript(messages: room.messages).displayed == visible)
#expect(room.snapshot.room.messages.count == 3)
#expect(room.unreadCount == 1)
#expect(room.snapshot.room.readCursor == "0")
#expect(room.snapshot.isTyping)
#expect(room.snapshot.room.activity == activity)
}
let tool = try #require(room.snapshot.room.messages.first(where: { $0.id == "tool" })?.toolCallMetadata)
#expect(tool.status == "completed")
#expect(tool.args == .object(["query": .string("fixture")]))
#expect(tool.resultPreview == "Raw preview")
#expect(tool.result == .object(["matches": .number(3)]))
#expect(tool.startedAt == "start")
#expect(room.snapshot.room.cursor == "5")
// The rendered snapshot's cursor includes hidden tool updates. A later
// reply must remain unread when only that captured cursor is acknowledged.
room.isSelected = true
let capturedCursor = room.snapshot.room.cursor
let lateReply = agentMessage("late-reply", entry: "6")
let lateTool = agentMessage("late-tool", entry: "7", role: .tool)
try room.receive(.messages(agentPage([lateReply, lateTool], cursor: "7"))); room.flush()
room.markRead(renderedCursor: capturedCursor)
#expect(await eventually { room.snapshot.room.readCursor == "5" })
#expect(room.snapshot.room.cursor == "7")
#expect(room.unreadCount == 1)
#expect(AgentTranscript(messages: room.messages).displayed == visible + [lateReply])
#expect(!room.snapshot.isTyping)
#expect(room.isBusy)
let socket = try #require(await client.latestSocket())
await socket.push(#"{"type":"error","error":"Room connection unavailable"}"#)
#expect(await eventually { room.error == "Room connection unavailable" })
#expect(AgentTranscript(messages: room.messages).displayed == visible + [lateReply])
#expect(room.snapshot.room.messages.count == 5)
}
@Test @MainActor func failedSendDetailsAndRetryStayVisibleWithHiddenTools() async throws {
let tool = agentMessage("tool", entry: "1", role: .tool)
let client = TestAgentClient(pages: ["0": agentPage([tool], cursor: "1")])
await client.setFailures(submit: true)
let memory = MemoryOutbox()
let room = SybilAgentRoom(); room.configure(client: client, storage: memory.storage)
defer { room.stop() }
#expect(await eventually { room.connection == "Connected" })
#expect(room.send("Please retry the search tool"))
#expect(await eventually { room.outbox.first?.status == "failed" })
let failed = try #require(AgentTranscript(messages: room.messages).displayed.first)
let requestID = try #require(room.outbox.first?.requestID)
#expect(failed.role == .user)
#expect(failed.content == "Please retry the search tool")
#expect(failed.metadata?.objectValue?["status"] == .string("failed"))
#expect(failed.metadata?.objectValue?["error"] == .string("Lost acknowledgement"))
#expect(AgentTranscript(messages: room.messages).displayed.count == 1)
#expect(memory.read().first?.requestID == requestID)
await client.setFailures()
room.retry(failed.id)
#expect(await eventually { room.outbox.first?.status == "queued" })
let retried = try #require(AgentTranscript(messages: room.messages).displayed.first)
#expect(retried.id == failed.id)
#expect(retried.createdAt == failed.createdAt)
#expect(retried.metadata?.objectValue?["status"] == .string("queued"))
#expect(retried.metadata?.objectValue?["error"] == nil)
#expect(await client.submittedIDs() == [requestID, requestID])
#expect(room.snapshot.room.messages == [tool])
#expect(room.messages.count == 2)
}
@Test func cursorAndMalformedFrames() throws {
#expect(try AgentCursor.normalize("00012") == "12")
#expect(try AgentCursor.advance("9", "10") == "10")