///|
/// Mutable state for one isolated agent run.
pub struct Session {
policy : Policy
mut state : SessionState
mut attempts : Int
tool_attempts : Map[String, Int]
mut used_bytes : Int
mut reserved_bytes : Int
events : Array[AuditEvent]
approvals : Array[ApprovalRequest]
}
///|
/// Starts a fresh session governed by `policy`.
pub fn Session::new(policy : Policy) -> Session {
{
policy,
state: Active,
attempts: 0,
tool_attempts: Map([]),
used_bytes: 0,
reserved_bytes: 0,
events: [],
approvals: [],
}
}
///|
/// Returns the current lifecycle state.
pub fn Session::state(self : Session) -> SessionState {
self.state
}
///|
/// Closes this session. Closing an already closed session has no effect.
pub fn Session::close(self : Session) -> Unit {
self.state = Closed
for request in self.approvals {
self.replace_audit_event(request.id, ApprovalCancelled(request.id), 0)
}
self.approvals.clear()
self.reserved_bytes = 0
}
///|
/// Returns a defensive copy of operations awaiting human approval.
pub fn Session::pending_approvals(self : Session) -> Array[ApprovalRequest] {
let result = []
for request in self.approvals {
result.push({ id: request.id, operation: request.operation.snapshot(), })
}
result
}
///|
/// Approves a pending operation and charges its reserved byte estimate.
pub fn Session::approve(self : Session, request_id : Int) -> Decision {
self.resolve_approval(request_id, true)
}
///|
/// Rejects a pending operation and releases its reserved byte estimate.
pub fn Session::reject(self : Session, request_id : Int) -> Decision {
self.resolve_approval(request_id, false)
}
///|
/// Authorizes an operation and records the result.
///
/// Every attempt consumes one call slot. Operations configured for human
/// approval reserve their byte estimate until approved or rejected. This
/// method never performs the operation itself.
pub fn Session::authorize(self : Session, operation : Operation) -> Decision {
let tool = operation.tool_name()
let tool_attempts = match self.tool_attempts.get(tool) {
Some(count) => count
None => 0
}
let policy_decision = match self.state {
Closed => Deny(SessionClosed)
Active =>
self.policy.decide(
self.attempts,
tool_attempts,
self.used_bytes + self.reserved_bytes,
operation,
)
}
let sequence = self.attempts + 1
let decision = match policy_decision {
Allow if self.policy.requires_approval(tool) => {
self.reserved_bytes = self.reserved_bytes + operation.byte_cost()
self.approvals.push({ id: sequence, operation: operation.snapshot(), })
ApprovalRequired(sequence)
}
_ => policy_decision
}
self.attempts = sequence
self.tool_attempts[tool] = tool_attempts + 1
let charged_bytes = if decision is Allow { operation.byte_cost() } else { 0 }
self.used_bytes = self.used_bytes + charged_bytes
self.events.push({
sequence,
operation: operation.snapshot(),
decision,
charged_bytes,
})
decision
}
///|
/// Number of attempted operations, including denied ones.
pub fn Session::attempts(self : Session) -> Int {
self.attempts
}
///|
/// Bytes charged by allowed operations.
pub fn Session::used_bytes(self : Session) -> Int {
self.used_bytes
}
///|
/// Remaining call slots in this session.
pub fn Session::remaining_calls(self : Session) -> Int {
let remaining = self.policy.max_calls - self.attempts
if remaining > 0 {
remaining
} else {
0
}
}
///|
/// Remaining I/O bytes, including reservations for pending approvals.
pub fn Session::remaining_bytes(self : Session) -> Int {
self.policy.max_io_bytes - self.used_bytes - self.reserved_bytes
}
///|
/// Returns a defensive copy of the audit log.
pub fn Session::audit_log(self : Session) -> Array[AuditEvent] {
self.events.copy()
}
///|
fn Session::resolve_approval(
self : Session,
request_id : Int,
approved : Bool,
) -> Decision {
if self.state is Closed {
return Deny(SessionClosed)
}
let mut found_index = -1
for index, request in self.approvals {
if request.id == request_id {
found_index = index
}
}
if found_index < 0 {
return Deny(ApprovalNotPending(request_id~))
}
let request = self.approvals[found_index]
let byte_cost = request.operation.byte_cost()
self.reserved_bytes = self.reserved_bytes - byte_cost
let decision = if approved {
self.used_bytes = self.used_bytes + byte_cost
ApprovalGranted(request_id)
} else {
Deny(ApprovalRejected(request_id~))
}
ignore(self.approvals.remove(found_index))
self.replace_audit_event(
request_id,
decision,
if approved {
byte_cost
} else {
0
},
)
decision
}
///|
fn Session::replace_audit_event(
self : Session,
sequence : Int,
decision : Decision,
charged_bytes : Int,
) -> Unit {
let index = sequence - 1
if index >= 0 && index < self.events.length() {
let event = self.events[index]
self.events[index] = {
sequence: event.sequence,
operation: event.operation,
decision,
charged_bytes,
}
}
}