@@ -41,8 +41,8 @@ extension ResponsesAPIClient {
4141 stallTimeout: Duration ? ,
4242 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
4343 ) async throws where S. Element == UInt8 {
44- let completionState = StreamCompletionState ( )
45- let semanticState = ResponsesStreamingSemanticState ( )
44+ let completionState = ResponsesStreamCompletionState ( )
45+ let semanticState = ResponsesStreamState ( )
4646 try await processSSEStream ( bytes: bytes, stallTimeout: stallTimeout) { [ self ] line in
4747 let didComplete = try await handleSSELine (
4848 line,
@@ -64,7 +64,7 @@ extension ResponsesAPIClient {
6464 private func handleSSELine(
6565 _ line: String ,
6666 messagesCount: Int ,
67- semanticState: ResponsesStreamingSemanticState ,
67+ semanticState: ResponsesStreamState ,
6868 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
6969 ) async throws -> Bool {
7070 try Task . checkCancellation ( )
@@ -88,7 +88,7 @@ extension ResponsesAPIClient {
8888 _ type: String ,
8989 data: Data ,
9090 messagesCount: Int ,
91- semanticState: ResponsesStreamingSemanticState ,
91+ semanticState: ResponsesStreamState ,
9292 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
9393 ) async throws -> Bool {
9494 switch type {
@@ -104,8 +104,9 @@ extension ResponsesAPIClient {
104104 semanticState: semanticState,
105105 continuation: continuation
106106 )
107- case " response.function_call_arguments.delta " :
108- try await handleFunctionCallArgsDelta (
107+ case " response.function_call_arguments.delta " ,
108+ " response.custom_tool_call_input.delta " :
109+ try await handleToolCallArgsDelta (
109110 data: data,
110111 semanticState: semanticState,
111112 continuation: continuation
@@ -139,7 +140,7 @@ extension ResponsesAPIClient {
139140
140141 private func handleTextDelta(
141142 data: Data ,
142- semanticState: ResponsesStreamingSemanticState ,
143+ semanticState: ResponsesStreamState ,
143144 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
144145 ) async throws {
145146 let event = try Self . sseDecoder. decode (
@@ -154,33 +155,44 @@ extension ResponsesAPIClient {
154155
155156 private func handleOutputItemAdded(
156157 data: Data ,
157- semanticState: ResponsesStreamingSemanticState ,
158+ semanticState: ResponsesStreamState ,
158159 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
159160 ) async throws {
160161 let event = try Self . sseDecoder. decode (
161162 OutputItemAddedEvent . self, from: data
162163 )
163- guard event. item. type == " function_call " ,
164- let callId = event. item. callId,
165- let name = event. item. name
166- else { return }
164+ let kind : ToolCallKind
165+ switch event. item. type {
166+ case " function_call " :
167+ kind = . function
168+ case " custom_tool_call " :
169+ kind = . custom
170+ case " mcp_call " , " computer_call " , " apply_patch_call " :
171+ throw AgentError . llmError ( . featureUnsupported(
172+ provider: " responses " ,
173+ feature: " \( event. item. type) streaming "
174+ ) )
175+ default :
176+ return
177+ }
178+ guard let callId = event. item. callId, let name = event. item. name else { return }
167179 let delta = StreamDelta . toolCallStart (
168180 index: event. outputIndex,
169181 id: callId,
170182 name: name,
171- kind: . function
183+ kind: kind
172184 )
173185 await semanticState. record ( delta)
174186 continuation. yield ( . delta( delta) )
175187 }
176188
177- private func handleFunctionCallArgsDelta (
189+ private func handleToolCallArgsDelta (
178190 data: Data ,
179- semanticState: ResponsesStreamingSemanticState ,
191+ semanticState: ResponsesStreamState ,
180192 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
181193 ) async throws {
182194 let event = try Self . sseDecoder. decode (
183- FunctionCallArgsDeltaEvent . self, from: data
195+ ToolCallArgsDeltaEvent . self, from: data
184196 )
185197 if !event. delta. isEmpty {
186198 let delta = StreamDelta . toolCallDelta (
@@ -194,7 +206,7 @@ extension ResponsesAPIClient {
194206
195207 private func handleReasoningSummaryDelta(
196208 data: Data ,
197- semanticState: ResponsesStreamingSemanticState ,
209+ semanticState: ResponsesStreamState ,
198210 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
199211 ) async throws {
200212 let event = try Self . sseDecoder. decode (
@@ -216,7 +228,7 @@ extension ResponsesAPIClient {
216228
217229 private func handleOutputItemDone(
218230 data: Data ,
219- semanticState: ResponsesStreamingSemanticState ,
231+ semanticState: ResponsesStreamState ,
220232 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
221233 ) async throws {
222234 let value = try Self . sseDecoder. decode (
@@ -231,7 +243,7 @@ extension ResponsesAPIClient {
231243 private func handleCompleted(
232244 data: Data ,
233245 messagesCount: Int ,
234- semanticState: ResponsesStreamingSemanticState ,
246+ semanticState: ResponsesStreamState ,
235247 continuation: AsyncThrowingStream < RunStreamElement , Error > . Continuation
236248 ) async throws -> Bool {
237249 let event : CompletedEvent
@@ -291,7 +303,7 @@ private struct OutputItemStub: Decodable {
291303 enum CodingKeys : String , CodingKey { case type, callId = " call_id " , name }
292304}
293305
294- private struct FunctionCallArgsDeltaEvent : Decodable {
306+ private struct ToolCallArgsDeltaEvent : Decodable {
295307 let outputIndex : Int
296308 let delta : String
297309 enum CodingKeys : String , CodingKey { case outputIndex = " output_index " , delta }
@@ -323,172 +335,3 @@ private struct OutputItemDoneItem: Decodable {
323335private struct CompletedEvent : Decodable { let response : ResponsesAPIResponse }
324336private struct FailedEvent : Decodable { let response : FailedResponseBody }
325337private struct FailedResponseBody : Decodable { let error : ResponsesErrorDetail ? }
326-
327- private actor StreamCompletionState {
328- private( set) var isCompleted = false
329- func markCompleted( ) {
330- isCompleted = true
331- }
332- }
333-
334- private actor ResponsesStreamingSemanticState {
335- private var content = " "
336- private var reasoning = " "
337- private var reasoningDetails : [ JSONValue ] = [ ]
338- private var toolCalls : [ Int : ResponsesStreamedToolCall ] = [ : ]
339- private var lastSummaryPart : ( outputIndex: Int , summaryIndex: Int ) ?
340- private var summaryPartCount = 0
341-
342- func summaryPartSeparator( forOutput outputIndex: Int ? , summary summaryIndex: Int ? ) -> String ? {
343- guard let summaryIndex else { return nil }
344- if let outputIndex {
345- let current = ( outputIndex, summaryIndex)
346- defer { lastSummaryPart = current }
347- guard let last = lastSummaryPart,
348- current. 0 != last. outputIndex || current. 1 != last. summaryIndex
349- else { return nil }
350- return " \n "
351- }
352- defer { summaryPartCount += 1 }
353- return summaryPartCount > 0 ? " \n " : nil
354- }
355-
356- func record( _ delta: StreamDelta ) {
357- switch delta {
358- case let . content( text) :
359- content += text
360- case let . reasoning( text) :
361- reasoning += text
362- case let . reasoningDetails( details) :
363- reasoningDetails. append ( contentsOf: details)
364- case let . toolCallStart( index, id, name, _) :
365- toolCalls [ index] = ResponsesStreamedToolCall (
366- id: id,
367- name: name,
368- arguments: toolCalls [ index] ? . arguments ?? " "
369- )
370- case let . toolCallDelta( index, arguments) :
371- let existing = toolCalls [ index]
372- toolCalls [ index] = ResponsesStreamedToolCall (
373- id: existing? . id,
374- name: existing? . name,
375- arguments: ( existing? . arguments ?? " " ) + arguments
376- )
377- case . audioData, . audioTranscript, . audioStarted, . finished:
378- break
379- }
380- }
381-
382- func reconciliationDeltas(
383- response: ResponsesAPIResponse ,
384- projection: ResponsesAPIClient . ResponsesTurnProjection
385- ) throws -> [ StreamDelta ] {
386- let target = try ResponsesCompletedSemanticTarget ( response: response, projection: projection)
387- var deltas : [ StreamDelta ] = [ ]
388-
389- guard let reasoningSuffix = utf8Suffix ( of: target. reasoning, afterPrefix: reasoning) else {
390- throw AgentError . malformedStream ( . finalizedSemanticStateDiverged)
391- }
392- if !reasoningSuffix. isEmpty {
393- reasoning += reasoningSuffix
394- deltas. append ( . reasoning( reasoningSuffix) )
395- }
396-
397- guard target. reasoningDetails. count >= reasoningDetails. count,
398- Array ( target. reasoningDetails. prefix ( reasoningDetails. count) ) == reasoningDetails
399- else {
400- throw AgentError . malformedStream ( . finalizedSemanticStateDiverged)
401- }
402- let reasoningDetailSuffix = Array ( target. reasoningDetails. dropFirst ( reasoningDetails. count) )
403- if !reasoningDetailSuffix. isEmpty {
404- reasoningDetails += reasoningDetailSuffix
405- deltas. append ( . reasoningDetails( reasoningDetailSuffix) )
406- }
407-
408- guard let contentSuffix = utf8Suffix ( of: target. content, afterPrefix: content) else {
409- throw AgentError . malformedStream ( . finalizedSemanticStateDiverged)
410- }
411- if !contentSuffix. isEmpty {
412- content += contentSuffix
413- deltas. append ( . content( contentSuffix) )
414- }
415-
416- let targetIndices = Set ( target. toolCalls. keys)
417- guard Set ( toolCalls. keys) . isSubset ( of: targetIndices) else {
418- throw AgentError . malformedStream ( . finalizedSemanticStateDiverged)
419- }
420-
421- for (index, targetCall) in target. toolCalls. sorted ( by: { $0. key < $1. key } ) {
422- if let existing = toolCalls [ index] {
423- guard existing. id == nil || existing. id == targetCall. id,
424- existing. name == nil || existing. name == targetCall. name,
425- let argumentsSuffix = utf8Suffix (
426- of: targetCall. arguments,
427- afterPrefix: existing. arguments
428- )
429- else {
430- throw AgentError . malformedStream ( . finalizedSemanticStateDiverged)
431- }
432- if existing. id == nil , let id = targetCall. id, let name = targetCall. name {
433- deltas. append ( . toolCallStart( index: index, id: id, name: name, kind: . function) )
434- }
435- if !argumentsSuffix. isEmpty {
436- toolCalls [ index] = ResponsesStreamedToolCall (
437- id: existing. id ?? targetCall. id,
438- name: existing. name ?? targetCall. name,
439- arguments: existing. arguments + argumentsSuffix
440- )
441- deltas. append ( . toolCallDelta( index: index, arguments: argumentsSuffix) )
442- }
443- } else if let id = targetCall. id, let name = targetCall. name {
444- toolCalls [ index] = targetCall
445- deltas. append ( . toolCallStart( index: index, id: id, name: name, kind: . function) )
446- if !targetCall. arguments. isEmpty {
447- deltas. append ( . toolCallDelta( index: index, arguments: targetCall. arguments) )
448- }
449- }
450- }
451-
452- return deltas
453- }
454- }
455-
456- private func utf8Suffix( of target: String , afterPrefix prefix: String ) -> String ? {
457- let targetBytes = Array ( target. utf8)
458- let prefixBytes = Array ( prefix. utf8)
459- guard targetBytes. starts ( with: prefixBytes) else { return nil }
460- return String ( bytes: targetBytes. dropFirst ( prefixBytes. count) , encoding: . utf8)
461- }
462-
463- private struct ResponsesStreamedToolCall : Equatable {
464- let id : String ?
465- let name : String ?
466- let arguments : String
467- }
468-
469- private struct ResponsesCompletedSemanticTarget {
470- let content : String
471- let reasoning : String
472- let reasoningDetails : [ JSONValue ]
473- let toolCalls : [ Int : ResponsesStreamedToolCall ]
474-
475- init (
476- response: ResponsesAPIResponse ,
477- projection: ResponsesAPIClient . ResponsesTurnProjection
478- ) throws {
479- content = projection. content
480- reasoning = projection. reasoning? . content ?? " "
481- reasoningDetails = projection. reasoningDetails ?? [ ]
482-
483- var indexedToolCalls : [ Int : ResponsesStreamedToolCall ] = [ : ]
484- for (index, outputItem) in response. output. enumerated ( ) {
485- guard case let . functionCall( call) = outputItem else { continue }
486- indexedToolCalls [ index] = ResponsesStreamedToolCall (
487- id: call. callId,
488- name: call. name,
489- arguments: call. arguments
490- )
491- }
492- toolCalls = indexedToolCalls
493- }
494- }
0 commit comments