Add Model retry

This commit is contained in:
Kaede Akino committed 2026-09-15 09:21:25 +08:00
1 parent 11b963008e
commit 04b676bf06
15 files changed
+363 -37

No files matched your search

@@ -3101,6 +3101,22 @@
}
}
},
"Agent error" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Agent错误"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "Agent錯誤"
}
}
}
},
"Archive" : {
"localizations" : {
"zh-Hans" : {
@@ -17838,6 +17854,22 @@
}
}
},
"Reconnecting… %lld/%lld" : {
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "正在重新连接…%1$lld/%2$lld"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "正在重新連線…%1$lld/%2$lld"
}
}
}
},
"Reconnecting · attempt %lld" : {
"extractionState" : "stale",
"localizations" : {
@@ -115,17 +115,27 @@ public final class AssistantExecutionController {
guard !userPrompt.isEmpty || !userImages.isEmpty || !userContextAttachments.isEmpty
else { return }
guard let chat = ai.resolve(conversation: conversation.model) else {
conversation.messages.append(
ChatMessage(
role: .assistant, text: String(localized: "No Chat model is configured.")
))
recordFailedRun(
conversation,
promptID: promptID ?? lastUserTurn?.id ?? UUID(),
prompt: userPrompt,
authoredPrompt: authoredPrompt ?? lastUserTurn?.authoredText,
images: userImages,
contextAttachments: userContextAttachments,
reason: String(localized: "No Chat model is configured.")
)
return
}
guard let secret = ai.secret(for: chat.provider.effectiveCredentialRef), !secret.isEmpty else {
conversation.messages.append(
ChatMessage(
role: .assistant, text: String(localized: "The Chat model has no available credential.")
))
recordFailedRun(
conversation,
promptID: promptID ?? lastUserTurn?.id ?? UUID(),
prompt: userPrompt,
authoredPrompt: authoredPrompt ?? lastUserTurn?.authoredText,
images: userImages,
contextAttachments: userContextAttachments,
reason: String(localized: "The Chat model has no available credential.")
)
return
}
@@ -268,6 +278,36 @@ public final class AssistantExecutionController {
scheduleTitle(for: conversation)
}
private func recordFailedRun(
_ conversation: ConversationModel,
promptID: UUID,
prompt: String,
authoredPrompt: String?,
images: [AgentImageAttachment],
contextAttachments: [AgentContextAttachment],
reason: String
) {
var messages = conversation.pendingPriorMessages
if !messages.contains(where: { $0.id == promptID }) {
messages.append(
AgentTranscriptMessage(
id: promptID, role: .user, text: prompt,
authoredText: authoredPrompt, images: images,
contextAttachments: contextAttachments,
contextSnapshot: conversation.transcript.first(where: { $0.id == promptID })?.contextSnapshot
))
}
let now = Date.now
let snapshot = AgentRunSnapshot(
conversationID: conversation.id, state: .failed,
permissionMode: ai.resolve(conversation: conversation.permissionMode),
messages: messages, startedAt: now, finishedAt: now,
failure: reason
)
conversation.recordFailedRun(snapshot)
Task { [agentRunRepository] in try? await agentRunRepository.save(snapshot) }
}
public func contextSnapshot(
for conversation: ConversationModel, context: SessionContext, includesTerminalSuggestion: Bool
) -> AgentTurnContextSnapshot {
@@ -277,6 +277,8 @@ nonisolated public struct ChatMessage: Identifiable, Hashable, Sendable {
public var contextChips: [ContextChipReference] = []
public var images: [AgentImageAttachment] = []
public var commands: [SuggestedCommand] = []
/// Provider retry state is live presentation, never conversation history.
public var retryProgress: AgentModelRetryProgress?
/// True while tokens are still arriving, so the view can show a caret.
public var isStreaming: Bool = false
/// Orders this message against the tool cards, which are a separate list.
@@ -287,7 +289,8 @@ nonisolated public struct ChatMessage: Identifiable, Hashable, Sendable {
public init(
id: UUID = UUID(), role: Role, text: String, reasoning: [AgentReasoningBlock] = [],
contextChips: [ContextChipReference] = [], images: [AgentImageAttachment] = [],
commands: [SuggestedCommand] = [], isStreaming: Bool = false, createdAt: Date = .now
commands: [SuggestedCommand] = [], retryProgress: AgentModelRetryProgress? = nil,
isStreaming: Bool = false, createdAt: Date = .now
) {
self.id = id
self.role = role
@@ -296,6 +299,7 @@ nonisolated public struct ChatMessage: Identifiable, Hashable, Sendable {
self.contextChips = contextChips
self.images = images
self.commands = commands
self.retryProgress = retryProgress
self.isStreaming = isStreaming
self.createdAt = createdAt
}
@@ -374,6 +378,24 @@ nonisolated public struct PendingSteeringMessage: Identifiable, Hashable, Sendab
}
}
nonisolated public struct AgentRunFailureNotice: Identifiable, Hashable, Sendable {
public var runID: UUID
public var reason: String
public var createdAt: Date
public var id: UUID {
AgentTranscriptMessage.toolResultID(
runID: runID, callID: "__termind_agent_failure_notice__"
)
}
public init(runID: UUID, reason: String, createdAt: Date) {
self.runID = runID
self.reason = reason
self.createdAt = createdAt
}
}
/// One row of a conversation, whichever of the two lists it came from.
nonisolated public enum TimelineItem: Identifiable, Hashable, Sendable {
case message(ChatMessage)
@@ -383,6 +405,7 @@ nonisolated public enum TimelineItem: Identifiable, Hashable, Sendable {
case plan(AgentPlanCard)
case compaction(AgentCompactionMark)
case compactionActivity(AgentCompactionActivity)
case failure(AgentRunFailureNotice)
public var id: UUID {
switch self {
@@ -393,6 +416,7 @@ nonisolated public enum TimelineItem: Identifiable, Hashable, Sendable {
case .plan(let card): card.id
case .compaction(let mark): mark.id
case .compactionActivity(let activity): activity.id
case .failure(let failure): failure.id
}
}
@@ -407,6 +431,7 @@ nonisolated public enum TimelineItem: Identifiable, Hashable, Sendable {
case .plan(let card): card.createdAt
case .compaction(let mark): mark.createdAt
case .compactionActivity(let activity): activity.createdAt
case .failure(let failure): failure.createdAt
}
}
@@ -418,6 +443,7 @@ nonisolated public enum TimelineItem: Identifiable, Hashable, Sendable {
switch self {
case .work: -1
case .compaction, .compactionActivity: 1
case .failure: 2
default: 0
}
}
@@ -498,10 +524,12 @@ nonisolated public struct AgentWorkGroup: Identifiable, Hashable, Sendable {
/// The provider summary currently receiving deltas. The view scans this
/// text instead of showing a second generic Working indicator outside.
public var activeReasoningBlockID: AgentReasoningBlock.ID?
public var retryProgress: AgentModelRetryProgress?
public init(
run: AgentRunSummary, items: [AgentWorkItem], orderedAt: Date, anchorMessageID: UUID? = nil,
appearsAfterAnswer: Bool, activeReasoningBlockID: AgentReasoningBlock.ID? = nil
appearsAfterAnswer: Bool, activeReasoningBlockID: AgentReasoningBlock.ID? = nil,
retryProgress: AgentModelRetryProgress? = nil
) {
self.run = run
self.items = items
@@ -509,10 +537,20 @@ nonisolated public struct AgentWorkGroup: Identifiable, Hashable, Sendable {
self.anchorMessageID = anchorMessageID
self.appearsAfterAnswer = appearsAfterAnswer
self.activeReasoningBlockID = activeReasoningBlockID
self.retryProgress = retryProgress
}
}
nonisolated public enum AgentWorkPresentation {
public static func reconnectingTitle(
_ progress: AgentModelRetryProgress, locale: Locale = .current
) -> String {
AppLocalization.string(
"Reconnecting… \(progress.attempt)/\(progress.maximumAttempts)",
locale: locale
)
}
public static func isExpandedByDefault(
for state: AgentRunState, appearsAfterAnswer: Bool = false,
isCompact _: Bool = false
@@ -1303,6 +1341,7 @@ public final class ConversationModel: Identifiable {
let activeReasoningBlockID = processMessages.first(where: {
$0.id == activeReasoningMessageID && $0.isStreaming && $0.text.isEmpty
})?.reasoning.last?.id
let retryProgress = processMessages.compactMap(\.retryProgress).last
var items =
reasoningItems + visibleProcess.map(AgentWorkItem.message)
+ cards.map(AgentWorkItem.tool)
@@ -1357,7 +1396,8 @@ public final class ConversationModel: Identifiable {
return AgentWorkGroup(
run: run, items: items, orderedAt: orderedAt,
anchorMessageID: anchorID, appearsAfterAnswer: answerAt != nil,
activeReasoningBlockID: activeReasoningBlockID
activeReasoningBlockID: activeReasoningBlockID,
retryProgress: retryProgress
)
}.sorted { $0.orderedAt < $1.orderedAt }
}
@@ -1432,8 +1472,17 @@ public final class ConversationModel: Identifiable {
}
items += groups.filter { !emittedGroups.contains($0.id) }.map(TimelineItem.work)
let commands = toolCards.compactMap(\.terminalSuggestion)
let failures = runSummaries.values.compactMap { run -> AgentRunFailureNotice? in
guard run.state == .failed, let reason = run.failure, !reason.isEmpty else {
return nil
}
return AgentRunFailureNotice(
runID: run.id, reason: reason,
createdAt: run.finishedAt ?? run.startedAt
)
}
guard
!commands.isEmpty || !plans.isEmpty || !compactions.isEmpty
!commands.isEmpty || !plans.isEmpty || !compactions.isEmpty || !failures.isEmpty
|| compactionActivity != nil
else {
return items
@@ -1446,6 +1495,7 @@ public final class ConversationModel: Identifiable {
commands.map(TimelineItem.command)
+ plans.map(TimelineItem.plan)
+ compactions.map(TimelineItem.compaction)
+ failures.map(TimelineItem.failure)
if let compactionActivity { extra.append(.compactionActivity(compactionActivity)) }
return (items + extra).sorted {
$0.orderedAt == $1.orderedAt
@@ -1829,6 +1879,25 @@ public final class ConversationModel: Identifiable {
}
}
/// Records a request that could not construct a runtime, such as a missing
/// model or credential, using the same durable run failure UI as a provider
/// error that arrived after dispatch.
public func recordFailedRun(_ snapshot: AgentRunSnapshot) {
guard snapshot.conversationID == id, snapshot.state == .failed,
let failure = snapshot.failure
else { return }
if let prompt = snapshot.messages.last(where: { $0.role == .user }) {
messageRunIDs[prompt.id] = snapshot.id
}
runSummaries[snapshot.id] = AgentRunSummary(
id: snapshot.id, state: .failed, startedAt: snapshot.startedAt,
finishedAt: snapshot.finishedAt, failure: failure
)
reportedRunState = .failed
updatedAt = snapshot.finishedAt ?? .now
runDidFinish?()
}
/// Retains the ordered event consumer until the cancellation snapshot is durable.
public func stopAndWait() async {
guard let runtime else {
@@ -2316,6 +2385,7 @@ public final class ConversationModel: Identifiable {
continue
}
messages[index].isStreaming = false
messages[index].retryProgress = nil
}
contextEstimateTracker.clearStreamingText()
publishContextEstimateImmediately()
@@ -2367,6 +2437,30 @@ public final class ConversationModel: Identifiable {
createdAt: started.createdAt
))
}
case .modelRetryScheduled(let progress):
activeReasoningMessageID = nil
if let index = messages.firstIndex(where: { $0.id == progress.messageID }) {
messages[index].text = ""
messages[index].reasoning = []
messages[index].commands = []
messages[index].retryProgress = progress
messages[index].isStreaming = true
} else {
messages.append(
ChatMessage(
id: progress.messageID, role: .assistant, text: "",
retryProgress: progress, isStreaming: true
))
}
messageRunIDs[progress.messageID] = progress.runID
contextEstimateTracker.clearStreamingText()
publishContextEstimateImmediately()
case .modelRetryRecovered(let progress):
if let index = messages.firstIndex(where: { $0.id == progress.messageID }),
messages[index].retryProgress == progress
{
messages[index].retryProgress = nil
}
case .messageDelta(let id, let text):
if let activeRunID, messageRunIDs[id] != activeRunID {
messageRunIDs[id] = activeRunID
@@ -2412,6 +2506,7 @@ public final class ConversationModel: Identifiable {
if let index = messages.firstIndex(where: { $0.id == message.id }) {
messages[index].text = message.text
messages[index].reasoning = message.reasoning
messages[index].retryProgress = nil
messages[index].isStreaming = false
} else {
messages.append(
@@ -2508,7 +2603,11 @@ public final class ConversationModel: Identifiable {
case .tasksUpdated(let updated):
tasks = updated
case .failed(let reason):
messages.append(ChatMessage(role: .assistant, text: reason))
guard let activeRunID, var run = runSummaries[activeRunID] else { break }
run.state = .failed
run.finishedAt = .now
run.failure = reason
runSummaries[activeRunID] = run
}
}
@@ -259,7 +259,7 @@ public final class SwiftDataAgentRunRepository: AgentRunPersisting, Sendable {
guard let state = AgentRunState(rawValue: run.state) else { return nil }
return AgentRunSummary(
id: run.id, state: state, startedAt: run.startedAt,
finishedAt: run.finishedAt
finishedAt: run.finishedAt, failure: run.failure
)
},
toolCards: cards,
@@ -456,7 +456,8 @@ public final class SwiftDataAgentRunRepository: AgentRunPersisting, Sendable {
AgentRunSnapshot(
id: summary.id, conversationID: value.id, state: summary.state,
permissionMode: .askForApproval, messages: [],
startedAt: summary.startedAt, finishedAt: summary.finishedAt
startedAt: summary.startedAt, finishedAt: summary.finishedAt,
failure: summary.failure
)))
}
let validRunIDs = Set(summaries.map(\.id))
@@ -14,6 +14,7 @@ nonisolated public struct AssistantTranscriptEntry: Identifiable, Hashable, Send
case plan
case compaction
case compactionActivity
case failure
case interrupted
}
@@ -27,6 +28,7 @@ nonisolated public struct AssistantTranscriptEntry: Identifiable, Hashable, Send
case plan(AgentPlanCard)
case compaction(AgentCompactionMark)
case compactionActivity(AgentCompactionActivity)
case failure(AgentRunFailureNotice)
case interrupted
}
@@ -63,6 +65,7 @@ nonisolated public struct AssistantTranscriptEntry: Identifiable, Hashable, Send
case .plan(let card): .plan(card)
case .compaction(let mark): .compaction(mark)
case .compactionActivity(let activity): .compactionActivity(activity)
case .failure(let failure): .failure(failure)
}
return Self(id: item.id, content: content, hasLeadingSpacing: index > 0)
}
@@ -93,6 +96,7 @@ nonisolated public struct AssistantTranscriptEntry: Identifiable, Hashable, Send
case .plan: .plan
case .compaction: .compaction
case .compactionActivity: .compactionActivity
case .failure: .failure
case .interrupted: .interrupted
}
}
@@ -9,11 +9,12 @@ nonisolated public enum AssistantTranscriptDisclosureID: Hashable, Sendable {
case reasoning(UUID)
case plan(UUID)
case compactionActivity(UUID)
case failure(UUID)
fileprivate var sourceID: UUID {
switch self {
case .work(let id), .tool(let id), .toolBatch(let id), .reasoning(let id), .plan(let id),
.compactionActivity(let id):
.compactionActivity(let id), .failure(let id):
id
}
}
+8
View File
@@ -176,6 +176,14 @@ preflight, card creation, hooks, authorization, then execution, and
OpenAI Responses/Chat Completions, Anthropic Messages, and Gemini. Models without
tool capability receive no tools; only recognized chat-purpose models appear in
conversation pickers, and unknown purpose values safely default to chat.
Ordinary planning and acting runs have no turn or tool-call count ceiling; they end
when the model finishes, the user stops, a hook terminates, or an error occurs.
Guardian remains separately bounded so unavailable or inconclusive review fails closed.
Each ordinary model turn retries only transient provider transport/service failures
five times at 1/2/4/8/16 seconds. A retry reuses the logical message and request,
abandons its partial presentation, and cannot replay a tool because tool dispatch begins
only after a complete model response. Retry progress is ephemeral; final run failures
remain durable and render as error rows rather than assistant-authored text.
AgentKit's callable `fetch` and `scratch_fetch` tools are `.ask` because each
contacts a caller-selected URL. Explicitly enabled `web_search` remains
pre-approved; a provider-native search card is a post-hoc record of a search the
@@ -646,6 +646,8 @@ struct MobileAssistantConversation: View {
timelineRow(.compaction(mark))
case .compactionActivity(let activity):
timelineRow(.compactionActivity(activity))
case .failure(let failure):
agentFailureRow(failure)
case .interrupted:
Label("This run was interrupted and was not resumed.", systemImage: "pause.circle")
.font(.caption).foregroundStyle(.secondary)
@@ -669,6 +671,8 @@ struct MobileAssistantConversation: View {
case .compactionActivity(let activity): compactionActivityRow(activity)
case .compaction(let mark):
compactionRow(mark)
case .failure(let failure):
agentFailureRow(failure)
}
}
@@ -736,6 +740,39 @@ struct MobileAssistantConversation: View {
}
}
private func agentFailureRow(_ failure: AgentRunFailureNotice) -> some View {
DisclosureGroup(isExpanded: disclosureBinding(for: .failure(failure.id))) {
HStack(alignment: .top, spacing: 12) {
Rectangle()
.fill(Color(uiColor: .separator))
.frame(width: 1)
.accessibilityHidden(true)
Text(failure.reason)
.font(.caption.monospaced())
.foregroundStyle(.red)
.textSelection(.enabled)
.frame(maxWidth: .infinity, alignment: .leading)
Button("Copy", systemImage: "doc.on.doc") {
UIPasteboard.general.string = failure.reason
}
.labelStyle(.iconOnly)
.buttonStyle(.plain)
.foregroundStyle(.secondary)
}
.padding(.leading, 24)
} label: {
HStack(spacing: 8) {
Image(systemName: "exclamationmark.triangle")
.foregroundStyle(.red)
Text("Agent error")
.foregroundStyle(.primary)
}
.font(.callout)
}
.tint(.secondary)
.accessibilityHint(Text("Show failure details"))
}
private var imageTileSize: CGFloat { sizeClass == .compact ? 104 : 160 }
private func messageRow(_ message: ChatMessage) -> some View {
@@ -798,7 +835,12 @@ struct MobileAssistantConversation: View {
}
}
if message.isStreaming && message.text.isEmpty && message.reasoning.isEmpty {
MobileAssistantActivity(cyclesText: true)
MobileAssistantActivity(
cyclesText: message.retryProgress == nil,
reconnectingTitle: message.retryProgress.map {
AgentWorkPresentation.reconnectingTitle($0, locale: locale)
}
)
}
if !message.isStreaming { messageActions(message) }
ForEach(message.commands) { command in MobileAssistantCommandRow(command: command) }
@@ -80,6 +80,7 @@ struct MobileAssistantScanning: ViewModifier {
struct MobileAssistantActivity: View {
var cyclesText = false
var reconnectingTitle: String? = nil
@State private var startedAt = Date.now
@Environment(\.mobileListRowIsDisplayed) private var displayed
@Environment(\.mobileListReduceMotion) private var reduceMotion
@@ -87,7 +88,9 @@ struct MobileAssistantActivity: View {
var body: some View {
Group {
if cyclesText && displayed && !reduceMotion && sceneIsActive {
if let reconnectingTitle {
Text(reconnectingTitle)
} else if cyclesText && displayed && !reduceMotion && sceneIsActive {
TimelineView(.animation(minimumInterval: AssistantTranscriptStyle.activityTextDuration)) { context in
let titles: [LocalizedStringKey] = ["Working…", "Thinking…", "Reasoning…"]
let index =
@@ -102,7 +105,10 @@ struct MobileAssistantActivity: View {
Text("Working…")
}
}
.foregroundStyle(.secondary)
.foregroundStyle(
reconnectingTitle == nil
? AnyShapeStyle(.secondary) : AnyShapeStyle(.tertiary)
)
.modifier(MobileAssistantScanning(active: true))
.frame(maxWidth: .infinity, alignment: .leading)
}
+18 -5
View File
@@ -36,7 +36,11 @@ struct MobileAssistantWorkRow: View {
.modifier(MobileAssistantScanning(active: group.activeReasoningBlockID == block.id))
case .message(let message):
if message.text.isEmpty && message.isStreaming {
MobileAssistantActivity()
MobileAssistantActivity(
reconnectingTitle: group.retryProgress.map {
AgentWorkPresentation.reconnectingTitle($0, locale: locale)
}
)
} else if !message.text.isEmpty {
MobileAssistantMarkdown(text: message.text)
}
@@ -54,11 +58,20 @@ struct MobileAssistantWorkRow: View {
private func workTitle(now: Date) -> some View {
Text(
AgentWorkPresentation.title(
for: group.run.state, from: group.run.startedAt, to: group.run.finishedAt ?? now, locale: locale
)
group.retryProgress.map {
AgentWorkPresentation.reconnectingTitle($0, locale: locale)
}
?? AgentWorkPresentation.title(
for: group.run.state, from: group.run.startedAt,
to: group.run.finishedAt ?? now, locale: locale
)
)
.font(.callout).foregroundStyle(.secondary).monospacedDigit()
.font(.callout)
.foregroundStyle(
group.retryProgress == nil
? AnyShapeStyle(.secondary) : AnyShapeStyle(.tertiary)
)
.monospacedDigit()
}
}
+9
View File
@@ -442,6 +442,15 @@ struct TranscriptListView: NSViewRepresentable {
.configure { [weak self] row, entry, context in
self?.configure(row, entry: entry, width: context.width, measurement: false)
}
ListRow(TranscriptFailureRow.self)
.when { $0.kind == .failure }
.estimatedHeight(48)
.height { [weak self] entry, context in
self?.height(for: entry, width: context.width) ?? 48
}
.configure { [weak self] row, entry, context in
self?.configure(row, entry: entry, width: context.width, measurement: false)
}
ListRow(TranscriptInterruptedRow.self)
.when { $0.kind == .interrupted }
.estimatedHeight(96)
+80 -10
View File
@@ -179,6 +179,7 @@ class TranscriptNativeRow: ListRowView {
case .plan: TranscriptPlanRow()
case .compaction: TranscriptCompactionRow()
case .compactionActivity: TranscriptCompactionActivityRow()
case .failure: TranscriptFailureRow()
case .interrupted: TranscriptInterruptedRow()
}
}
@@ -370,6 +371,7 @@ class TranscriptNativeRow: ListRowView {
@MainActor final class TranscriptPlanRow: TranscriptNativeRow {}
@MainActor final class TranscriptCompactionRow: TranscriptNativeRow {}
@MainActor final class TranscriptCompactionActivityRow: TranscriptNativeRow {}
@MainActor final class TranscriptFailureRow: TranscriptNativeRow {}
@MainActor final class TranscriptInterruptedRow: TranscriptNativeRow {}
@MainActor
@@ -441,13 +443,21 @@ private enum NativeTranscriptFactory {
stack.addArrangedSubview(markdownView)
}
if message.isStreaming, message.text.isEmpty {
let retry = message.retryProgress
let title = retry.map {
AgentWorkPresentation.reconnectingTitle(
$0, locale: context.configuration.locale
)
} ?? String(localized: "Working…")
let activity = NativeActivityLabel(
text: String(localized: "Working…"),
alternatives: [
String(localized: "Working…"),
String(localized: "Thinking…"),
String(localized: "Reasoning…"),
],
text: title,
alternatives: retry == nil
? [
String(localized: "Working…"),
String(localized: "Thinking…"),
String(localized: "Reasoning…"),
] : [title],
color: retry == nil ? .secondaryLabelColor : .tertiaryLabelColor,
reduceMotion: context.configuration.reduceMotion,
measurement: context.measurement
)
@@ -527,6 +537,8 @@ private enum NativeTranscriptFactory {
return compaction(mark)
case .compactionActivity(let activity):
return compactionActivity(activity, context: context)
case .failure(let failure):
return failureRow(failure, context: context)
}
}
@@ -834,7 +846,12 @@ private enum NativeTranscriptFactory {
)
let expanded = context.presentation.disclosures.isExpanded(.work(group.id), default: defaultExpanded)
let titleProvider = {
AgentWorkPresentation.title(
if let retry = group.retryProgress {
return AgentWorkPresentation.reconnectingTitle(
retry, locale: context.configuration.locale
)
}
return AgentWorkPresentation.title(
for: group.run.state,
from: group.run.startedAt,
to: group.run.finishedAt ?? .now,
@@ -844,8 +861,9 @@ private enum NativeTranscriptFactory {
let header = NativeDisclosureHeader(
symbol: nil,
title: titleProvider(),
titleColor: .secondaryLabelColor,
titleColor: group.retryProgress == nil ? .secondaryLabelColor : .tertiaryLabelColor,
expanded: expanded,
active: group.retryProgress != nil,
reduceMotion: context.configuration.reduceMotion,
measurement: context.measurement,
lineLimit: 1,
@@ -877,10 +895,17 @@ private enum NativeTranscriptFactory {
workItems.addArrangedSubview(reasoning)
case .message(let message):
if message.text.isEmpty, message.isStreaming {
let title = group.retryProgress.map {
AgentWorkPresentation.reconnectingTitle(
$0, locale: context.configuration.locale
)
} ?? String(localized: "Working…")
workItems.addArrangedSubview(
NativeActivityLabel(
text: String(localized: "Working…"),
alternatives: [String(localized: "Working…")],
text: title,
alternatives: [title],
color: group.retryProgress == nil
? .secondaryLabelColor : .tertiaryLabelColor,
reduceMotion: context.configuration.reduceMotion,
measurement: context.measurement
))
@@ -1149,6 +1174,51 @@ private enum NativeTranscriptFactory {
return stack
}
private static func failureRow(
_ failure: AgentRunFailureNotice,
context: NativeTranscriptContext
) -> NSView {
let stack = NativeStack.vertical()
let disclosureID = AssistantTranscriptDisclosureID.failure(failure.id)
let expanded = context.presentation.disclosures.isExpanded(disclosureID)
let header = NativeDisclosureHeader(
symbol: "exclamationmark.triangle",
title: String(localized: "Agent error"),
iconColor: .systemRed,
titleColor: .labelColor,
expanded: expanded,
reduceMotion: context.configuration.reduceMotion,
measurement: context.measurement,
lineLimit: 1
)
header.toolTip = String(localized: "Show failure details")
header.handler = {
context.presentation.disclosures.toggleDisclosure(disclosureID)
context.invalidate(context.entryID, false)
}
stack.addArrangedSubview(header)
if expanded {
let detail = NativeStack.horizontal(spacing: Metrics.Space.m)
detail.edgeInsets = .init(
top: 0, left: Metrics.Sidebar.rowIcon, bottom: 0, right: 0
)
detail.addArrangedSubview(NativeVerticalRule())
detail.addArrangedSubview(
NativeLabel(
failure.reason,
font: .monospacedSystemFont(
ofSize: NSFont.smallSystemFontSize,
weight: .regular
),
color: .systemRed,
selectable: true
))
detail.addArrangedSubview(NativeCopyButton(text: failure.reason))
stack.addArrangedSubview(detail)
}
return stack
}
private static func markdown(
source: String,
slot: String,
+1
View File
@@ -538,6 +538,7 @@ struct ArchitectureBoundaryTests {
"AgentRuntime", "AgentToolExecutor", "AgentToolScheduler", "AgentTurnDriver",
"AgentToolCatalog", "AgentToolRegistry", "AgentEventChannel", "AgentContextPipeline",
"AgentSecretLifecycle", "AgentApprovalBroker", "SecretBroker", "AgentProviderClient",
"AgentModelRetryPolicy", "AgentModelRetryProgress",
"MCPClient", "MCPServer", "ModelProvider", "AIModel", "AgentSkill",
]
let declaration = try Regex(
+1 -1
View File
@@ -2595,7 +2595,7 @@
repositoryURL = "https://github.com/AkinoKaede/AgentKit.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 0.9.2;
minimumVersion = 0.11.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -6,8 +6,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/AkinoKaede/AgentKit.git",
"state" : {
"revision" : "ad72d1fd7f868d75fe417cf58c8f31c279ca09f7",
"version" : "0.9.2"
"revision" : "8ac6e78bab3185a0a6fc9d1af1bfeb2f8db57ca9",
"version" : "0.11.0"
}
},
{