Skip to content

Commit 870e167

Browse files
committed
add(core): transcript-grade StreamEvent with identity, timestamps, and stable Codable wire format
1 parent ec569df commit 870e167

36 files changed

Lines changed: 1900 additions & 238 deletions

Sources/AgentRunKit/Core/Agent+ContextBudget.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ extension Agent {
3030
guard var phase = budgetPhase else { return }
3131
let result = phase.afterResponse(usage: usage, messages: &messages)
3232
budgetPhase = phase
33-
continuation?.yield(.budgetUpdated(budget: result.budget))
33+
continuation?.yield(.make(.budgetUpdated(budget: result.budget)))
3434
if result.advisoryEmitted {
35-
continuation?.yield(.budgetAdvisory(budget: result.budget))
35+
continuation?.yield(.make(.budgetAdvisory(budget: result.budget)))
3636
}
3737
}
3838

@@ -54,7 +54,7 @@ extension Agent {
5454
}
5555
}
5656
messages.append(.tool(id: call.id, name: call.name, content: result.content))
57-
continuation?.yield(.toolCallCompleted(id: call.id, name: call.name, result: result))
57+
continuation?.yield(.make(.toolCallCompleted(id: call.id, name: call.name, result: result)))
5858
}
5959
}
6060

@@ -174,7 +174,9 @@ extension Agent {
174174
try Task.checkCancellation()
175175

176176
for entry in denied {
177-
continuation.yield(.toolCallCompleted(id: entry.call.id, name: entry.call.name, result: entry.result))
177+
continuation.yield(.make(.toolCallCompleted(
178+
id: entry.call.id, name: entry.call.name, result: entry.result
179+
)))
178180
allResults.append(entry)
179181
}
180182

Sources/AgentRunKit/Core/Agent+ToolApproval.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ extension Agent {
4949
arguments: indexed.call.arguments,
5050
toolDescription: tool.description
5151
)
52-
continuation?.yield(.toolApprovalRequested(request))
52+
continuation?.yield(.make(.toolApprovalRequested(request)))
5353
let decision = try await awaitApprovalDecision(for: request, using: handler)
54-
continuation?.yield(.toolApprovalResolved(toolCallId: indexed.call.id, decision: decision))
54+
continuation?.yield(.make(.toolApprovalResolved(toolCallId: indexed.call.id, decision: decision)))
5555

5656
switch decision {
5757
case .approve:

Sources/AgentRunKit/Core/Agent+ToolExecution.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,15 +64,15 @@ extension Agent {
6464
continuation: AsyncThrowingStream<StreamEvent, Error>.Continuation,
6565
approvalHandler: ToolApprovalHandler? = nil
6666
) async throws -> ToolResult {
67-
continuation.yield(.subAgentStarted(toolCallId: call.id, toolName: call.name))
67+
continuation.yield(.make(.subAgentStarted(toolCallId: call.id, toolName: call.name)))
6868

6969
var result = ToolResult.error("Sub-agent did not complete")
7070
defer {
71-
continuation.yield(.subAgentCompleted(toolCallId: call.id, toolName: call.name, result: result))
71+
continuation.yield(.make(.subAgentCompleted(toolCallId: call.id, toolName: call.name, result: result)))
7272
}
7373

7474
let eventHandler: @Sendable (StreamEvent) -> Void = { event in
75-
continuation.yield(.subAgentEvent(toolCallId: call.id, toolName: call.name, event: event))
75+
continuation.yield(.make(.subAgentEvent(toolCallId: call.id, toolName: call.name, event: event)))
7676
}
7777

7878
do {
@@ -118,7 +118,7 @@ extension Agent {
118118

119119
var results = [(Int, ToolCall, ToolResult)]()
120120
for try await (index, call, result) in group {
121-
continuation.yield(.toolCallCompleted(id: call.id, name: call.name, result: result))
121+
continuation.yield(.make(.toolCallCompleted(id: call.id, name: call.name, result: result)))
122122
results.append((index, call, result))
123123
}
124124
return results.sorted { $0.0 < $1.0 }.map { ($0.1, $0.2) }

Sources/AgentRunKit/Core/Agent.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ private extension Agent {
278278
&messages, lastTotalTokens: lastTotalTokens, totalUsage: &totalUsage
279279
)
280280
if compacted, let totalTokens = lastTotalTokens, let windowSize = client.contextWindowSize {
281-
continuation.yield(.compacted(totalTokens: totalTokens, windowSize: windowSize))
281+
continuation.yield(.make(.compacted(totalTokens: totalTokens, windowSize: windowSize)))
282282
}
283283
let iteration = try await processor.process(
284284
messages: messages,
@@ -289,7 +289,7 @@ private extension Agent {
289289

290290
if let usage = iteration.usage {
291291
lastTotalTokens = usage.total
292-
continuation.yield(.iterationCompleted(usage: usage, iteration: iterationNumber))
292+
continuation.yield(.make(.iterationCompleted(usage: usage, iteration: iterationNumber)))
293293
}
294294

295295
messages.append(.assistant(iteration.toAssistantMessage()))
@@ -354,20 +354,20 @@ private extension Agent {
354354
from toolCalls: [ToolCall], tokenUsage: TokenUsage, history: [ChatMessage]
355355
) throws -> StreamEvent {
356356
guard let finishCall = toolCalls.first(where: { $0.name == "finish" }) else {
357-
return .finished(tokenUsage: tokenUsage, content: nil, reason: nil, history: history)
357+
return .make(.finished(tokenUsage: tokenUsage, content: nil, reason: nil, history: history))
358358
}
359359
let decoded: FinishArguments
360360
do {
361361
decoded = try JSONDecoder().decode(FinishArguments.self, from: finishCall.argumentsData)
362362
} catch {
363363
throw AgentError.finishDecodingFailed(message: String(describing: error))
364364
}
365-
return .finished(
365+
return .make(.finished(
366366
tokenUsage: tokenUsage,
367367
content: decoded.content,
368368
reason: FinishReason(decoded.reason ?? "completed"),
369369
history: history
370-
)
370+
))
371371
}
372372

373373
func parseFinishResult(

Sources/AgentRunKit/Core/AgentResult.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,38 @@ public enum FinishReason: Sendable, Equatable, CustomStringConvertible {
2323
}
2424
}
2525

26+
extension FinishReason: Codable {
27+
private enum CodingKeys: String, CodingKey {
28+
case type, value
29+
}
30+
31+
public init(from decoder: any Decoder) throws {
32+
let container = try decoder.container(keyedBy: CodingKeys.self)
33+
let type = try container.decode(String.self, forKey: .type)
34+
switch type {
35+
case "completed": self = .completed
36+
case "error": self = .error
37+
case "custom": self = try .custom(container.decode(String.self, forKey: .value))
38+
default:
39+
throw DecodingError.dataCorruptedError(
40+
forKey: .type, in: container,
41+
debugDescription: "Unknown FinishReason type: \(type)"
42+
)
43+
}
44+
}
45+
46+
public func encode(to encoder: any Encoder) throws {
47+
var container = encoder.container(keyedBy: CodingKeys.self)
48+
switch self {
49+
case .completed: try container.encode("completed", forKey: .type)
50+
case .error: try container.encode("error", forKey: .type)
51+
case let .custom(value):
52+
try container.encode("custom", forKey: .type)
53+
try container.encode(value, forKey: .value)
54+
}
55+
}
56+
}
57+
2658
public struct AgentResult: Sendable, Equatable {
2759
public let finishReason: FinishReason
2860
public let content: String

Sources/AgentRunKit/Core/AgentStream.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ public final class AgentStream<C: ToolContext> {
111111
}
112112

113113
private func handle(_ event: StreamEvent, toolCallIdPath: [String], toolNamePath: [String]) {
114-
switch event {
114+
switch event.kind {
115115
case let .delta(text):
116116
content += text
117117
case let .reasoningDelta(text):

Sources/AgentRunKit/Core/Chat.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,9 @@ public struct Chat<C: ToolContext>: Sendable {
202202
messages.append(.assistant(iteration.toAssistantMessage()))
203203

204204
if policy.shouldTerminateAfterIteration(toolCalls: iteration.toolCalls) {
205-
continuation.yield(.finished(tokenUsage: totalUsage, content: nil, reason: nil, history: messages))
205+
continuation.yield(.make(.finished(
206+
tokenUsage: totalUsage, content: nil, reason: nil, history: messages
207+
)))
206208
continuation.finish()
207209
return
208210
}
@@ -212,7 +214,7 @@ public struct Chat<C: ToolContext>: Sendable {
212214
call, context: context, approvalHandler: approvalHandler,
213215
allowlist: &sessionAllowlist, continuation: continuation
214216
)
215-
continuation.yield(.toolCallCompleted(id: call.id, name: call.name, result: result))
217+
continuation.yield(.make(.toolCallCompleted(id: call.id, name: call.name, result: result)))
216218
messages.append(.tool(id: call.id, name: call.name, content: result.content))
217219
}
218220
}
@@ -263,9 +265,9 @@ private extension Chat {
263265
toolCallId: call.id, toolName: call.name,
264266
arguments: call.arguments, toolDescription: tool.description
265267
)
266-
continuation.yield(.toolApprovalRequested(request))
268+
continuation.yield(.make(.toolApprovalRequested(request)))
267269
let decision = try await awaitApprovalDecision(for: request, using: handler)
268-
continuation.yield(.toolApprovalResolved(toolCallId: call.id, decision: decision))
270+
continuation.yield(.make(.toolApprovalResolved(toolCallId: call.id, decision: decision)))
269271
try Task.checkCancellation()
270272

271273
switch decision {

Sources/AgentRunKit/Core/ContextBudget.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,39 @@ public struct ContextBudget: Sendable, Equatable {
5050
}
5151
}
5252

53+
extension ContextBudget: Codable {
54+
private enum CodingKeys: String, CodingKey {
55+
case windowSize, currentUsage, softThreshold
56+
}
57+
58+
public init(from decoder: any Decoder) throws {
59+
let container = try decoder.container(keyedBy: CodingKeys.self)
60+
let windowSize = try container.decode(Int.self, forKey: .windowSize)
61+
let currentUsage = try container.decode(Int.self, forKey: .currentUsage)
62+
let softThreshold = try container.decodeIfPresent(Double.self, forKey: .softThreshold)
63+
guard windowSize >= 1 else {
64+
throw DecodingError.dataCorruptedError(
65+
forKey: .windowSize, in: container,
66+
debugDescription: "windowSize must be >= 1, got \(windowSize)"
67+
)
68+
}
69+
guard currentUsage >= 0 else {
70+
throw DecodingError.dataCorruptedError(
71+
forKey: .currentUsage, in: container,
72+
debugDescription: "currentUsage must be >= 0, got \(currentUsage)"
73+
)
74+
}
75+
self.init(windowSize: windowSize, currentUsage: currentUsage, softThreshold: softThreshold)
76+
}
77+
78+
public func encode(to encoder: any Encoder) throws {
79+
var container = encoder.container(keyedBy: CodingKeys.self)
80+
try container.encode(windowSize, forKey: .windowSize)
81+
try container.encode(currentUsage, forKey: .currentUsage)
82+
try container.encodeIfPresent(softThreshold, forKey: .softThreshold)
83+
}
84+
}
85+
5386
/// Configuration for context budget tracking and visibility.
5487
public struct ContextBudgetConfig: Sendable, Equatable {
5588
public let softThreshold: Double?
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import Foundation
2+
3+
private enum IdentifierCoding {
4+
static func decodeUUID(from decoder: any Decoder, typeName: String) throws -> UUID {
5+
let container = try decoder.singleValueContainer()
6+
let string = try container.decode(String.self)
7+
guard let uuid = UUID(uuidString: string) else {
8+
throw DecodingError.dataCorruptedError(
9+
in: container,
10+
debugDescription: "Invalid \(typeName) UUID string: \(string)"
11+
)
12+
}
13+
return uuid
14+
}
15+
16+
static func encodeUUID(_ uuid: UUID, to encoder: any Encoder) throws {
17+
var container = encoder.singleValueContainer()
18+
try container.encode(uuid.uuidString)
19+
}
20+
}
21+
22+
/// Uniquely identifies a streamed event.
23+
public struct EventID: Sendable, Hashable, Codable, CustomStringConvertible {
24+
public let rawValue: UUID
25+
26+
public init() {
27+
rawValue = UUID()
28+
}
29+
30+
public init(rawValue: UUID) {
31+
self.rawValue = rawValue
32+
}
33+
34+
public var description: String {
35+
rawValue.uuidString
36+
}
37+
38+
public init(from decoder: any Decoder) throws {
39+
rawValue = try IdentifierCoding.decodeUUID(from: decoder, typeName: "EventID")
40+
}
41+
42+
public func encode(to encoder: any Encoder) throws {
43+
try IdentifierCoding.encodeUUID(rawValue, to: encoder)
44+
}
45+
}
46+
47+
/// Uniquely identifies an agent session.
48+
public struct SessionID: Sendable, Hashable, Codable, CustomStringConvertible {
49+
public let rawValue: UUID
50+
51+
public init() {
52+
rawValue = UUID()
53+
}
54+
55+
public init(rawValue: UUID) {
56+
self.rawValue = rawValue
57+
}
58+
59+
public var description: String {
60+
rawValue.uuidString
61+
}
62+
63+
public init(from decoder: any Decoder) throws {
64+
rawValue = try IdentifierCoding.decodeUUID(from: decoder, typeName: "SessionID")
65+
}
66+
67+
public func encode(to encoder: any Encoder) throws {
68+
try IdentifierCoding.encodeUUID(rawValue, to: encoder)
69+
}
70+
}
71+
72+
/// Uniquely identifies an agent run within a session.
73+
public struct RunID: Sendable, Hashable, Codable, CustomStringConvertible {
74+
public let rawValue: UUID
75+
76+
public init() {
77+
rawValue = UUID()
78+
}
79+
80+
public init(rawValue: UUID) {
81+
self.rawValue = rawValue
82+
}
83+
84+
public var description: String {
85+
rawValue.uuidString
86+
}
87+
88+
public init(from decoder: any Decoder) throws {
89+
rawValue = try IdentifierCoding.decodeUUID(from: decoder, typeName: "RunID")
90+
}
91+
92+
public func encode(to encoder: any Encoder) throws {
93+
try IdentifierCoding.encodeUUID(rawValue, to: encoder)
94+
}
95+
}

0 commit comments

Comments
 (0)