Require a timeout on every agent tool
This commit is contained in:
@@ -30,6 +30,31 @@ struct Tool {
|
||||
}
|
||||
|
||||
let stdoutLock = NSLock()
|
||||
let maxTimeoutMs = 600_000
|
||||
|
||||
final class Once: @unchecked Sendable {
|
||||
private let lock = NSLock()
|
||||
private var done = false
|
||||
|
||||
func run(_ body: () -> Void) {
|
||||
let first = lock.withLock {
|
||||
defer { done = true }
|
||||
return !done
|
||||
}
|
||||
if first {
|
||||
body()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withTimeout(_ schema: String) -> Any {
|
||||
var s = try! JSONSerialization.jsonObject(with: Data(schema.utf8)) as! [String: Any]
|
||||
var props = s["properties"] as? [String: Any] ?? [:]
|
||||
props["timeout_ms"] = ["type": "integer", "description": "Give up on the call after this long (at most \(maxTimeoutMs))."]
|
||||
s["properties"] = props
|
||||
s["required"] = (s["required"] as? [String] ?? []) + ["timeout_ms"]
|
||||
return s
|
||||
}
|
||||
|
||||
let mediaDir: String? = {
|
||||
let args = CommandLine.arguments
|
||||
@@ -84,7 +109,7 @@ struct Server {
|
||||
resp["result"] = [:] as [String: Any]
|
||||
case "tools/list":
|
||||
resp["result"] = ["tools": tools.map { t in
|
||||
["name": t.name, "description": t.description, "inputSchema": try! JSONSerialization.jsonObject(with: Data(t.schema.utf8))]
|
||||
["name": t.name, "description": t.description, "inputSchema": withTimeout(t.schema)]
|
||||
}]
|
||||
case "tools/call":
|
||||
resp["result"] = await call(params)
|
||||
@@ -104,8 +129,28 @@ struct Server {
|
||||
guard let tool = tools.first(where: { $0.name == name }) else {
|
||||
return ["content": [Content.text("unknown tool: \(name)").json], "isError": true]
|
||||
}
|
||||
guard let ms = args.int("timeout_ms"), ms > 0, ms <= maxTimeoutMs else {
|
||||
return ["content": [Content.text("timeout_ms is required, between 1 and \(maxTimeoutMs)").json], "isError": true]
|
||||
}
|
||||
do {
|
||||
let content = try await withDisplayAwake { try await tool.run(args) }
|
||||
let content = try await withCheckedThrowingContinuation { (c: CheckedContinuation<[Content], Error>) in
|
||||
let once = Once()
|
||||
let work = Task {
|
||||
do {
|
||||
let content = try await withDisplayAwake { try await tool.run(args) }
|
||||
once.run { c.resume(returning: content) }
|
||||
} catch {
|
||||
once.run { c.resume(throwing: error) }
|
||||
}
|
||||
}
|
||||
Task {
|
||||
try? await Task.sleep(for: .milliseconds(ms))
|
||||
once.run {
|
||||
work.cancel()
|
||||
c.resume(throwing: ToolError("\(name) gave no result within \(ms)ms"))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ["content": content.map { $0.json }]
|
||||
} catch let e as ToolError {
|
||||
return ["content": [Content.text(e.message).json], "isError": true]
|
||||
|
||||
Reference in New Issue
Block a user