Human in the loop¶
A tool can ask before it acts. loopl pauses that one call inside the agent, emits an event and resumes when a person answers. Other tools in the same turn keep running.
Ask¶
A Tool returns an ApprovalRequest from requiresApproval(for:). HttpTool does for every request, graded by method — GET low, POST · PUT · PATCH medium, DELETE high — with credential headers redacted. open_app asks unless the scheme is allow-listed.
ApprovalPolicy decides what actually reaches the person: .mutating (the app's default) lets reads through and asks for writes, .always asks for everything, .never never asks. Trusted hosts pass.
Answer¶
import Loopl
func run(_ model: any Model) async throws {
let agent = Agent(model: model, tools: [HttpTool()],
hooks: [ApprovalPolicy(mode: .mutating, trustedHosts: ["api.example.com"])],
approvalTimeout: 120)
for try await event in agent.stream("Post today's summary to the notes API.") {
switch event {
case .approvalRequested(let request):
await agent.resolve(approval: request.id, .approve) // or .deny(reason:), .approveEdited(input)
case .toolResult(let result) where result.isError:
print(result.content) // a denial reaches the model as an error
default: break
}
}
}
No answer within approvalTimeout (120 s) denies; so does cancelling. agent.pendingApprovals lists what is waiting. An approval is an interrupt underneath.
In SwiftUI¶
AgentTranscriptView renders an ApprovalCard inline — method, host and path, redacted headers, the body, and Approve · Deny · Edit. Route answers with .onApproval { request, decision in … }. It springs in with LooplDesign.Motion and cross-fades under Reduce Motion.