Skip to content

Commit cb86522

Browse files
committed
Add agent loop with bash tool
1 parent c7e7137 commit cb86522

12 files changed

Lines changed: 1192 additions & 33 deletions

Package.swift

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,32 @@
44
import PackageDescription
55

66
let package = Package(
7-
name: "swift-claude-code",
8-
platforms: [.macOS(.v10_15)],
9-
products: [
10-
.executable(name: "claude", targets: ["cli"]),
11-
.library(name: "Core", targets: ["Core"])
12-
],
13-
dependencies: [
14-
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.32.0")
15-
],
16-
targets: [
17-
.executableTarget(
18-
name: "cli",
19-
dependencies: ["Core"],
20-
path: "Sources/cli"
21-
),
22-
.target(
23-
name: "Core",
24-
dependencies: [
25-
.product(name: "AsyncHTTPClient", package: "async-http-client")
26-
],
27-
path: "Sources/Core"
28-
),
29-
.testTarget(
30-
name: "CoreTests",
31-
dependencies: ["Core"],
32-
path: "Tests/CoreTests"
33-
),
34-
]
7+
name: "swift-claude-code",
8+
platforms: [.macOS(.v10_15)],
9+
products: [
10+
.executable(name: "claude", targets: ["cli"]),
11+
.library(name: "Core", targets: ["Core"]),
12+
],
13+
dependencies: [
14+
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.32.0")
15+
],
16+
targets: [
17+
.executableTarget(
18+
name: "cli",
19+
dependencies: ["Core"],
20+
path: "Sources/cli"
21+
),
22+
.target(
23+
name: "Core",
24+
dependencies: [
25+
.product(name: "AsyncHTTPClient", package: "async-http-client")
26+
],
27+
path: "Sources/Core"
28+
),
29+
.testTarget(
30+
name: "CoreTests",
31+
dependencies: ["Core"],
32+
path: "Tests/CoreTests"
33+
),
34+
]
3535
)

Sources/Core/ANSIColor.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
public enum ANSIColor: String, CustomStringConvertible {
2+
case cyan = "\u{001B}[36m"
3+
case yellow = "\u{001B}[33m"
4+
case dim = "\u{001B}[2m"
5+
case bold = "\u{001B}[1m"
6+
case red = "\u{001B}[31m"
7+
case reset = "\u{001B}[0m"
8+
9+
public var description: String { rawValue }
10+
}

Sources/Core/API/APIClient.swift

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import AsyncHTTPClient
2+
import Foundation
3+
import NIOCore
4+
import NIOFoundationCompat
5+
6+
public protocol APIClientProtocol {
7+
func createMessage(request: APIRequest) async throws -> APIResponse
8+
}
9+
10+
public struct APIClient: APIClientProtocol, Sendable {
11+
private let apiKey: String
12+
private let baseURL: String
13+
private let httpClient: HTTPClient
14+
15+
public init(
16+
apiKey: String,
17+
baseURL: String = "https://api.anthropic.com",
18+
httpClient: HTTPClient = .shared
19+
) {
20+
self.apiKey = apiKey
21+
self.baseURL = baseURL
22+
self.httpClient = httpClient
23+
}
24+
25+
public func createMessage(request: APIRequest) async throws -> APIResponse {
26+
let body = try JSONEncoder().encode(request)
27+
28+
var httpRequest = HTTPClientRequest(url: "\(baseURL)/v1/messages")
29+
httpRequest.method = .POST
30+
httpRequest.headers.add(name: "x-api-key", value: apiKey)
31+
httpRequest.headers.add(name: "anthropic-version", value: "2023-06-01")
32+
httpRequest.headers.add(name: "content-type", value: "application/json")
33+
httpRequest.body = .bytes(ByteBuffer(data: body))
34+
35+
let response = try await httpClient.execute(httpRequest, timeout: .seconds(300))
36+
let responseBody = try await response.body.collect(upTo: 10 * 1024 * 1024)
37+
let data = Data(buffer: responseBody)
38+
39+
guard (200..<300).contains(Int(response.status.code)) else {
40+
if let errorResponse = try? JSONDecoder().decode(APIErrorResponse.self, from: data) {
41+
throw errorResponse.error
42+
}
43+
throw APIClientError.httpError(
44+
statusCode: Int(response.status.code),
45+
body: String(data: data, encoding: .utf8) ?? "")
46+
}
47+
48+
return try JSONDecoder().decode(APIResponse.self, from: data)
49+
}
50+
}
51+
52+
public enum APIClientError: Error, CustomStringConvertible {
53+
case httpError(statusCode: Int, body: String)
54+
55+
public var description: String {
56+
switch self {
57+
case .httpError(let code, let body):
58+
return "HTTP \(code): \(body)"
59+
}
60+
}
61+
}

Sources/Core/API/APIModels.swift

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
import Foundation
2+
3+
// MARK: - Content blocks
4+
5+
public enum ContentBlock: Sendable, Equatable {
6+
case text(String)
7+
case toolUse(id: String, name: String, input: JSONValue)
8+
case toolResult(toolUseId: String, content: String, isError: Bool)
9+
}
10+
11+
extension ContentBlock: Codable {
12+
private enum BlockType: String, Codable {
13+
case text = "text"
14+
case toolUse = "tool_use"
15+
case toolResult = "tool_result"
16+
}
17+
18+
private enum CodingKeys: String, CodingKey {
19+
case type
20+
case text
21+
case id
22+
case name
23+
case input
24+
case toolUseId = "tool_use_id"
25+
case content
26+
case isError = "is_error"
27+
}
28+
29+
public init(from decoder: Decoder) throws {
30+
let container = try decoder.container(keyedBy: CodingKeys.self)
31+
let type = try container.decode(BlockType.self, forKey: .type)
32+
33+
switch type {
34+
case .text:
35+
let text = try container.decode(String.self, forKey: .text)
36+
self = .text(text)
37+
case .toolUse:
38+
let id = try container.decode(String.self, forKey: .id)
39+
let name = try container.decode(String.self, forKey: .name)
40+
let input = try container.decode(JSONValue.self, forKey: .input)
41+
self = .toolUse(id: id, name: name, input: input)
42+
case .toolResult:
43+
let toolUseId = try container.decode(String.self, forKey: .toolUseId)
44+
let content = try container.decodeIfPresent(String.self, forKey: .content) ?? ""
45+
let isError = try container.decodeIfPresent(Bool.self, forKey: .isError) ?? false
46+
self = .toolResult(toolUseId: toolUseId, content: content, isError: isError)
47+
}
48+
}
49+
50+
public func encode(to encoder: Encoder) throws {
51+
var container = encoder.container(keyedBy: CodingKeys.self)
52+
switch self {
53+
case .text(let text):
54+
try container.encode(BlockType.text, forKey: .type)
55+
try container.encode(text, forKey: .text)
56+
case .toolUse(let id, let name, let input):
57+
try container.encode(BlockType.toolUse, forKey: .type)
58+
try container.encode(id, forKey: .id)
59+
try container.encode(name, forKey: .name)
60+
try container.encode(input, forKey: .input)
61+
case .toolResult(let toolUseId, let content, let isError):
62+
try container.encode(BlockType.toolResult, forKey: .type)
63+
try container.encode(toolUseId, forKey: .toolUseId)
64+
try container.encode(content, forKey: .content)
65+
try container.encode(isError, forKey: .isError)
66+
}
67+
}
68+
}
69+
70+
extension Array where Element == ContentBlock {
71+
public var textContent: String {
72+
compactMap {
73+
if case .text(let value) = $0 {
74+
value
75+
} else {
76+
nil
77+
}
78+
}
79+
.joined(separator: "\n")
80+
}
81+
}
82+
83+
// MARK: - Messages
84+
85+
public struct Message: Codable, Sendable, Equatable {
86+
public enum Role: String, Codable, Sendable {
87+
case user
88+
case assistant
89+
}
90+
91+
public let role: Role
92+
public let content: [ContentBlock]
93+
94+
public init(role: Role, content: [ContentBlock]) {
95+
self.role = role
96+
self.content = content
97+
}
98+
99+
public static func user(_ text: String) -> Message {
100+
Message(role: .user, content: [.text(text)])
101+
}
102+
103+
public static func assistant(_ text: String) -> Message {
104+
Message(role: .assistant, content: [.text(text)])
105+
}
106+
}
107+
108+
// MARK: - Stop reason
109+
110+
public enum StopReason: String, Codable, Sendable {
111+
case endTurn = "end_turn"
112+
case toolUse = "tool_use"
113+
case maxTokens = "max_tokens"
114+
case stopSequence = "stop_sequence"
115+
}
116+
117+
// MARK: - Token usage
118+
119+
public struct Usage: Codable, Sendable {
120+
public let inputTokens: Int
121+
public let outputTokens: Int
122+
123+
private enum CodingKeys: String, CodingKey {
124+
case inputTokens = "input_tokens"
125+
case outputTokens = "output_tokens"
126+
}
127+
}
128+
129+
// MARK: - API response
130+
131+
public struct APIResponse: Codable, Sendable {
132+
public let id: String
133+
public let type: String
134+
public let role: String
135+
public let content: [ContentBlock]
136+
public let stopReason: StopReason?
137+
public let usage: Usage
138+
139+
private enum CodingKeys: String, CodingKey {
140+
case id, type, role, content, usage
141+
case stopReason = "stop_reason"
142+
}
143+
}
144+
145+
// MARK: - Tool definition
146+
147+
public struct ToolDefinition: Codable, Sendable {
148+
public let name: String
149+
public let description: String
150+
public let inputSchema: JSONValue
151+
152+
public init(name: String, description: String, inputSchema: JSONValue) {
153+
self.name = name
154+
self.description = description
155+
self.inputSchema = inputSchema
156+
}
157+
158+
private enum CodingKeys: String, CodingKey {
159+
case name, description
160+
case inputSchema = "input_schema"
161+
}
162+
}
163+
164+
// MARK: - API request
165+
166+
public struct APIRequest: Codable, Sendable {
167+
public let model: String
168+
public let maxTokens: Int
169+
public let system: String?
170+
public let messages: [Message]
171+
public let tools: [ToolDefinition]?
172+
173+
public init(
174+
model: String,
175+
maxTokens: Int = 4096,
176+
system: String? = nil,
177+
messages: [Message],
178+
tools: [ToolDefinition]? = nil
179+
) {
180+
self.model = model
181+
self.maxTokens = maxTokens
182+
self.system = system
183+
self.messages = messages
184+
self.tools = tools
185+
}
186+
187+
private enum CodingKeys: String, CodingKey {
188+
case model, system, messages, tools
189+
case maxTokens = "max_tokens"
190+
}
191+
}
192+
193+
// MARK: - API error
194+
195+
public struct APIError: Error, Codable, Sendable, CustomStringConvertible {
196+
public let type: String
197+
public let message: String
198+
199+
public var description: String {
200+
"\(type): \(message)"
201+
}
202+
}
203+
204+
public struct APIErrorResponse: Codable, Sendable {
205+
public let type: String
206+
public let error: APIError
207+
}

0 commit comments

Comments
 (0)