Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Sources/AnyLanguageModel/Generable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,13 @@ public macro Guide<RegexOutput>(
extension Generable {
/// The partially generated type of this struct.
public func asPartiallyGenerated() -> Self.PartiallyGenerated {
self as! Self.PartiallyGenerated
if let partial = self as? Self.PartiallyGenerated {
return partial
}
if let partial: Self.PartiallyGenerated = try? .init(self.generatedContent) {
return partial
}
fatalError("Unable to convert \(Self.self) to partially generated form")
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fatalError will crash the application when a type cannot be converted to its partially generated form. This is a critical runtime failure that could occur during normal streaming operations when partial data is invalid. Consider throwing an error instead of using fatalError, or return a fallback value that indicates the conversion failed, allowing the caller to handle the error gracefully.

Copilot uses AI. Check for mistakes.
}
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/AnyLanguageModel/GenerationSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ public struct GenerationSchema: Sendable, Codable, CustomDebugStringConvertible
}

let root: Node
private var defs: [String: Node]
var defs: [String: Node]

/// A string representation of the debug description.
///
Expand Down
60 changes: 31 additions & 29 deletions Sources/AnyLanguageModel/LanguageModelSession.swift
Original file line number Diff line number Diff line change
Expand Up @@ -758,21 +758,17 @@ extension LanguageModelSession {

extension LanguageModelSession {
public struct ResponseStream<Content>: Sendable where Content: Generable, Content.PartiallyGenerated: Sendable {
private let content: Content
private let rawContent: GeneratedContent
private let fallbackSnapshot: Snapshot?
private let streaming: AsyncThrowingStream<Snapshot, any Error>?

init(content: Content, rawContent: GeneratedContent) {
self.content = content
self.rawContent = rawContent
self.fallbackSnapshot = Snapshot(content: content.asPartiallyGenerated(), rawContent: rawContent)
self.streaming = nil
}

init(stream: AsyncThrowingStream<Snapshot, any Error>) {
// Fallback values when consumers call collect() before any snapshots arrive
// These will be replaced by the last yielded snapshot during collect()
self.content = (try? Content(GeneratedContent(""))) ?? ("" as! Content)
self.rawContent = GeneratedContent("")
// When streaming, snapshots arrive from the upstream sequence, so no fallback is required.
self.fallbackSnapshot = nil
self.streaming = stream
}

Expand All @@ -788,22 +784,14 @@ extension LanguageModelSession.ResponseStream: AsyncSequence {

public struct AsyncIterator: AsyncIteratorProtocol {
private var hasYielded = false
private let content: Content
private let rawContent: GeneratedContent
private let fallbackSnapshot: Snapshot?
private var streamIterator: AsyncThrowingStream<Snapshot, any Error>.AsyncIterator?
private let useStream: Bool

init(content: Content, rawContent: GeneratedContent, stream: AsyncThrowingStream<Snapshot, any Error>?) {
self.content = content
self.rawContent = rawContent
if let stream {
let iterator = stream.makeAsyncIterator()
self.streamIterator = iterator
self.useStream = true
} else {
self.streamIterator = nil
self.useStream = false
}
init(fallbackSnapshot: Snapshot?, stream: AsyncThrowingStream<Snapshot, any Error>?) {
self.fallbackSnapshot = fallbackSnapshot
self.streamIterator = stream?.makeAsyncIterator()
self.useStream = stream != nil
}

public mutating func next() async throws -> Snapshot? {
Expand All @@ -818,20 +806,17 @@ extension LanguageModelSession.ResponseStream: AsyncSequence {
}
return nil
} else {
guard !hasYielded else { return nil }
guard !hasYielded, let fallbackSnapshot else { return nil }
hasYielded = true
return Snapshot(
content: content.asPartiallyGenerated(),
rawContent: rawContent
)
return fallbackSnapshot
}
}

public typealias Element = Snapshot
}

public func makeAsyncIterator() -> AsyncIterator {
return AsyncIterator(content: content, rawContent: rawContent, stream: streaming)
return AsyncIterator(fallbackSnapshot: fallbackSnapshot, stream: streaming)
}

nonisolated public func collect() async throws -> sending LanguageModelSession.Response<Content> {
Expand All @@ -855,9 +840,26 @@ extension LanguageModelSession.ResponseStream: AsyncSequence {
)
}
}

if let fallbackSnapshot {
let finalContent: Content
if let concrete = fallbackSnapshot.content as? Content {
finalContent = concrete
} else {
finalContent = try Content(fallbackSnapshot.rawContent)
}
return LanguageModelSession.Response(
content: finalContent,
rawContent: fallbackSnapshot.rawContent,
transcriptEntries: []
)
}

// As a last resort, return an empty payload.
let empty = GeneratedContent("")
return LanguageModelSession.Response(
content: content,
rawContent: rawContent,
content: try Content(empty),
rawContent: empty,
transcriptEntries: []
)
Comment on lines +858 to 864
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last resort empty payload fallback in collect() attempts to create Content from an empty string, which will throw an error for most structured types. This means collect() can fail unexpectedly when no snapshots are received. The try keyword propagates this error, but the fallback should either return a proper error indicating no content was received, or ensure the empty GeneratedContent is valid for the Content type.

Copilot uses AI. Check for mistakes.
}
Expand Down
Loading