Skip to content

Commit ef8039e

Browse files
committed
refactor(agent-loop): consolidate shared loop helpers
1 parent 8890752 commit ef8039e

15 files changed

Lines changed: 436 additions & 208 deletions

Sources/AgentRunKit/Core/AgentLoop/Agent+ContextBudget.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,14 +149,6 @@ extension Agent {
149149
}
150150
}
151151

152-
func toolResultCharacterLimit(for toolName: String) -> Int? {
153-
firstTool(named: toolName, in: tools)?.maxResultCharacters ?? configuration.maxToolResultCharacters
154-
}
155-
156-
func truncatedToolResult(_ result: ToolResult, toolName: String) -> ToolResult {
157-
ContextCompactor.truncateToolResult(result, maxCharacters: toolResultCharacterLimit(for: toolName))
158-
}
159-
160152
private func partitionCallsRequiringApproval(
161153
_ calls: [IndexedToolCall],
162154
allowlist: Set<String>
@@ -187,7 +179,12 @@ extension Agent {
187179
IndexedToolResult(
188180
index: indexed.index,
189181
call: entry.call,
190-
result: truncatedToolResult(entry.result, toolName: entry.call.name)
182+
result: truncatedToolResult(
183+
entry.result,
184+
toolName: entry.call.name,
185+
tools: tools,
186+
fallbackLimit: configuration.maxToolResultCharacters
187+
)
191188
)
192189
}
193190
}
@@ -217,7 +214,12 @@ extension Agent {
217214
IndexedToolResult(
218215
index: entry.index,
219216
call: entry.call,
220-
result: truncatedToolResult(entry.result, toolName: entry.call.name)
217+
result: truncatedToolResult(
218+
entry.result,
219+
toolName: entry.call.name,
220+
tools: tools,
221+
fallbackLimit: configuration.maxToolResultCharacters
222+
)
221223
)
222224
}
223225
}

Sources/AgentRunKit/Core/AgentLoop/Agent+IterationHistory.swift

Lines changed: 0 additions & 54 deletions
This file was deleted.

Sources/AgentRunKit/Core/AgentLoop/Agent+StreamingLoop.swift

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
11
import Foundation
22

3-
struct StreamingLoopState {
4-
var messages: [ChatMessage]
5-
var historyWasRewrittenLocally: Bool = false
6-
var budgetPhase: ContextBudgetPhase?
7-
var sessionAllowlist: Set<String> = []
8-
}
9-
103
extension Agent {
114
func compactStreamingMessagesIfNeeded(
125
_ messages: inout [ChatMessage],
@@ -40,7 +33,7 @@ extension Agent {
4033
budgetUsage: TokenUsage?,
4134
options: InvocationOptions,
4235
continuation: AsyncThrowingStream<StreamEvent, Error>.Continuation,
43-
state: inout StreamingLoopState
36+
state: inout AgentLoopState
4437
) async throws {
4538
let indexedCalls = indexedExecutableToolCalls(from: toolCalls)
4639
let pruneCalls = indexedCalls.filter { $0.call.name == "prune_context" }

Sources/AgentRunKit/Core/AgentLoop/Agent+ToolApproval.swift

Lines changed: 0 additions & 71 deletions
This file was deleted.

Sources/AgentRunKit/Core/AgentLoop/Agent+ToolExecution.swift

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,74 @@
11
import Foundation
22

3+
struct IndexedToolCall {
4+
let index: Int
5+
let call: ToolCall
6+
}
7+
8+
struct IndexedToolResult {
9+
let index: Int
10+
let call: ToolCall
11+
let result: ToolResult
12+
}
13+
314
extension Agent {
15+
func requiresApproval(_ call: ToolCall, allowlist: Set<String>) -> Bool {
16+
configuration.approvalPolicy.requiresApproval(toolName: call.name, allowlist: allowlist)
17+
}
18+
19+
func resolveApprovals(
20+
_ calls: [IndexedToolCall],
21+
handler: @escaping ToolApprovalHandler,
22+
emit: StreamEmitter? = nil,
23+
allowlist: inout Set<String>
24+
) async throws -> (approved: [IndexedToolCall], denied: [IndexedToolResult]) {
25+
var approved: [IndexedToolCall] = []
26+
var denied: [IndexedToolResult] = []
27+
28+
for indexed in calls {
29+
guard let tool = firstTool(named: indexed.call.name, in: tools) else {
30+
approved.append(indexed)
31+
continue
32+
}
33+
34+
if allowlist.contains(indexed.call.name) {
35+
approved.append(indexed)
36+
continue
37+
}
38+
39+
let request = ToolApprovalRequest(
40+
toolCallId: indexed.call.id,
41+
toolName: indexed.call.name,
42+
arguments: indexed.call.arguments,
43+
toolDescription: tool.description
44+
)
45+
emit?.yield(.toolApprovalRequested(request))
46+
let decision = try await awaitApprovalDecision(for: request, using: handler)
47+
emit?.yield(.toolApprovalResolved(toolCallId: indexed.call.id, decision: decision))
48+
49+
switch decision {
50+
case .approve:
51+
approved.append(indexed)
52+
case .approveAlways:
53+
allowlist.insert(indexed.call.name)
54+
approved.append(indexed)
55+
case let .approveWithModifiedArguments(newArgs):
56+
let modified = ToolCall(
57+
id: indexed.call.id,
58+
name: indexed.call.name,
59+
arguments: newArgs,
60+
kind: indexed.call.kind
61+
)
62+
approved.append(IndexedToolCall(index: indexed.index, call: modified))
63+
case let .deny(reason):
64+
let result = ToolResult.error(reason ?? ToolFeedback.denied)
65+
denied.append(IndexedToolResult(index: indexed.index, call: indexed.call, result: result))
66+
}
67+
}
68+
69+
return (approved: approved, denied: denied)
70+
}
71+
472
func resolveTimeout(for call: ToolCall) -> Duration {
573
guard let tool = firstTool(named: call.name, in: tools) else {
674
return configuration.toolTimeout
@@ -27,7 +95,7 @@ extension Agent {
2795
} catch let error as AgentError {
2896
return ToolResult.error(error.feedbackMessage)
2997
} catch {
30-
return ToolResult.error("Tool failed: \(error)")
98+
return ToolResult.error(ToolFeedback.failed(error))
3199
}
32100
}
33101

@@ -41,7 +109,7 @@ extension Agent {
41109
let eventFactory = options.eventFactory
42110
continuation.yield(eventFactory.make(.subAgentStarted(toolCallId: call.id, toolName: call.name)))
43111

44-
let parentDepth = (context as? any CurrentDepthProviding)?.currentDepth ?? 0
112+
let parentDepth = currentDepth(of: context)
45113
let eventHandler: @Sendable (StreamEvent) -> Void = { [self] event in
46114
let processed = applyHistoryEmissionLimitToSubAgentEvent(event, parentDepth: parentDepth)
47115
continuation.yield(eventFactory.make(
@@ -63,7 +131,7 @@ extension Agent {
63131
} catch let error as AgentError {
64132
result = ToolResult.error(error.feedbackMessage)
65133
} catch {
66-
result = ToolResult.error("Tool failed: \(error)")
134+
result = ToolResult.error(ToolFeedback.failed(error))
67135
}
68136
continuation.yield(eventFactory.make(
69137
.subAgentCompleted(toolCallId: call.id, toolName: call.name, result: result)
@@ -95,7 +163,12 @@ extension Agent {
95163
} else {
96164
try await executeWithTimeout(call, context: context, approvalHandler: options.approvalHandler)
97165
}
98-
let truncated = truncatedToolResult(result, toolName: call.name)
166+
let truncated = truncatedToolResult(
167+
result,
168+
toolName: call.name,
169+
tools: tools,
170+
fallbackLimit: configuration.maxToolResultCharacters
171+
)
99172
continuation.yield(eventFactory.make(
100173
.toolCallCompleted(id: call.id, name: call.name, result: truncated)
101174
))
@@ -205,7 +278,12 @@ extension Agent {
205278

206279
var results = [(Int, ToolCall, ToolResult)]()
207280
for try await (index, call, result) in group {
208-
let truncated = truncatedToolResult(result, toolName: call.name)
281+
let truncated = truncatedToolResult(
282+
result,
283+
toolName: call.name,
284+
tools: tools,
285+
fallbackLimit: configuration.maxToolResultCharacters
286+
)
209287
continuation.yield(eventFactory.make(
210288
.toolCallCompleted(id: call.id, name: call.name, result: truncated)
211289
))

0 commit comments

Comments
 (0)