tiny.acp.ReaderStorage
Defined in tiny.acp.
A caller uses this type to read lines from a pipe or a file within a memory budget fixed in advance, as the client does with the agent's output.
API (22)
Actions
Public operations.
activate: A caller uses this function to mark the end of setup, after which the reader allocates nothing.commit: A caller uses this function to hand the bytes that arrived from its read to the reader.deinit: A caller uses this function to free the buffer when the caller is done reading.finish: A caller uses this function to tell the reader the input has ended.init: A caller uses this function to set up the reader, before the caller starts reading.poll: A caller's read loop calls this function first on each turn to take the next line.status: A caller uses this function to read the reader's sizes and counters at any time.writable: A caller uses this function to get the free space the caller's next read goes into.
Types and contracts
Public types and contracts.
Capacity: The capacity type,reader.Capacity.Exhaustion: The error set of a running reader,Exhaustion.InitError: The errorsinitreturns:error.OutOfMemoryfrom the allocator anderror.CapacityOverflowfrom working out the buffer size.Limits: The limits type,reader.Limits.claim: The repository's compile-time capacity checks read this declaration to hold the reader to the memory it states.
Fields and members
Public fields and members.
Source
Source: lib/acp/src/reader/storage.zig:62
zig
/// A caller uses this type to read lines from a pipe or a file within a memory budget fixed in/// advance, as the client does with the agent's output. The struct splits incoming bytes into lines/// inside one buffer that `init` allocates once. The caller moves the bytes: it polls for a line,/// and on `need_input` it reads into `writable` and passes the count to `commit`, or calls `finish`/// at the end of input. After `activate`, nothing the reader does allocates. A line past the limit/// leaves the reader terminal. The order of calls is `init`, `activate`, the steady calls, then/// `deinit`, and each call asserts the phase it needs. The struct keeps no allocator: `init` and/// `deinit` each take one. The package exports the type as `acp.ReaderStorage`.pub const Storage = struct { /// The reader's current lifecycle step. phase: alloc_phase.capacity.Phase, /// The line limit and buffer size worked out by `init`. capacity: capacity_mod.Capacity, /// The buffer, the buffer size long. The slice is empty after `deinit`. bytes: []u8, /// The index of the first byte not yet returned as a line. The index is 0 at the start and /// after each round of moving the unread bytes to the front of the buffer (compaction). start: usize = 0, /// The index one past the last byte read in. end: usize = 0, /// Set by `finish` when no more input will come. eof: bool = false, /// Whether the reader has stopped for good, set when a line runs past the limit, after which /// every `poll` fails with `error.ReaderMessageCapacityExceeded`. terminal: bool = false, /// The longest line seen, capped at the limit, in bytes. high_water_message_bytes: usize = 0, /// The number of lines rejected for running past the limit, which stops at the largest `u64`. rejected_message_count: u64 = 0, /// The limits type, `reader.Limits`. pub const Limits: type = capacity_mod.Limits; /// The capacity type, `reader.Capacity`. pub const Capacity: type = capacity_mod.Capacity; /// The error set of a running reader, `Exhaustion`. pub const Exhaustion: type = @import("storage.zig").Exhaustion; /// The errors `init` returns: `error.OutOfMemory` from the allocator and /// `error.CapacityOverflow` from working out the buffer size. Another program in the repository /// reuses it as its own `init` error set. pub const InitError = std.mem.Allocator.Error || capacity_mod.DeriveError; /// The repository's compile-time capacity checks read this declaration to hold the reader to /// the memory it states. The declaration provides a compile-time record of the reader's memory, /// with the id `acp.reader_storage`. It declares one region of the line limit plus one byte, /// sized by the caller's limit and held for the reader's steady life. The record lists what the /// region leaves out, in three groups. The first group covers threaded I/O, process pipes and /// kernel buffers. The second group covers parsed JSON, protocol results, updates and /// permission records. The third group covers outgoing messages and the capture of reply text. /// At overload, a line one byte past the limit ends the reading before any JSON parsing or call /// to the caller's callbacks for updates and permission decisions (observer). Splitting lines /// and compaction allocate nothing after activation, and a test witnesses it. Ten promises /// define the claim, each named by the test that witnesses it. The function `activate` seals /// the reader and `deinit` tears it down. A compile-time check at the end of the file checks /// the type's shape against the declaration. Each such test names its promise's key in a /// `@stardustClaim` block. pub const claim: alloc_phase.capacity.Declaration = .{ .source = .{ .id = "acp.reader_storage", .kind = .phase_static, .limit_source = .caller, .storage = .{ .covered = &.{ .{ .id = "one_reusable_acp_newline_framing_and_message_region", .lifetime = .steady, .detail = "one reusable ACP newline-framing and message region", }, }, .excluded = &.{ "threaded I/O, process pipes, and kernel buffering", "parsed JSON, protocol results, updates, and permission ownership", "outbound serialization and prompt capture", }, }, .capacity = .{ .inputs = &.{ alloc_phase.capacity.bindInput(Limits, "message_bytes", "message_bytes"), }, .type_selectors = &.{}, .nodes = &.{ .{ .input = 0 }, .{ .constant = 1 }, .{ .add = .{ .left = 0, .right = 1 } }, }, .assertions = &.{.{ .scope = .closure_total, .measure = .retained, .relation = .exact, .expression = 2, }}, }, .overload = .{ .kind = .terminal, .detail = "max plus one byte ends framing before JSON parsing or observer publication", }, .risks = .{ .transitive = .{ .status = .witnessed, .detail = "fragmented framing and compaction allocate nothing after activation", }, .foreign = .{ .status = .excluded, .detail = "process transport and kernel buffering are outside reader storage", }, }, .obligations = &.{ .{ .key = "acp_reader_capacity", .role = .capacity_model }, .{ .key = "acp_reader_acquisition", .role = .custom }, .{ .key = "acp_reader_oom", .role = .custom }, .{ .key = "acp_reader_boundary", .role = .overload }, .{ .key = "acp_reader_fragmented", .role = .custom }, .{ .key = "acp_reader_sealed", .role = .transitive_risk }, .{ .key = "acp_reader_client_terminal_overload", .role = .overload }, .{ .key = "acp_reader_client_terminal_foreign_risk", .role = .foreign_risk }, .{ .key = "acp_reader_admission", .role = .custom }, .{ .key = "acp_reader_root", .role = .custom }, }, }, .bindings = .{ .owner = @This(), .seal = .{ .family = alloc_phase.capacity.selector(@This().activate), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, .teardown = .{ .family = alloc_phase.capacity.selector(@This().deinit), .premise = .{ .class = .checked_semantic_fact, .authority = .checker, }, }, }, }; /// A caller uses this function to set up the reader, before the caller starts reading. The /// function allocates the buffer, the line limit plus one byte, in one allocation. The call /// returns the reader in its initialization phase, and the caller calls `activate` before the /// first poll. The function fails with `error.CapacityOverflow` before it allocates when the /// limit is too large to add one to, and with `error.OutOfMemory` when the allocation fails. /// The function leaves nothing allocated when it fails. The caller frees the buffer with /// `deinit` and the same allocator. pub fn init(allocator: std.mem.Allocator, limits: Limits) InitError!Storage { const capacity = try Capacity.derive(limits); const bytes = try allocator.alloc(u8, capacity.storage_bytes); const storage = Storage{ .phase = .initialization, .capacity = capacity, .bytes = bytes, }; std.debug.assert(storage.bytes.len == storage.capacity.storage_bytes); std.debug.assert(storage.capacity.storage_bytes > storage.capacity.message_bytes); return storage; } /// A caller uses this function to mark the end of setup, after which the reader allocates /// nothing. The call moves the reader from its initialization phase to its steady phase. The /// caller calls it once, after `init` and before the first poll. pub fn activate(self: *Storage) void { std.debug.assert(self.phase == .initialization); std.debug.assert(self.bytes.len == self.capacity.storage_bytes); self.phase = .steady; } /// A caller's read loop calls this function first on each turn to take the next line. The /// function returns the next whole line without its newline and moves past it. With no whole /// line buffered and more than the limit waiting, the call fails with /// `error.ReaderMessageCapacityExceeded` and the reader turns terminal. After `finish`, the /// function returns the remaining bytes as a last line, then `end`. Otherwise the function /// returns `need_input`. Once the reader is terminal, every call fails with the same error. A /// line of exactly the limit passes, and one byte more fails. The returned line stays valid /// until the next call to `writable` or `deinit`. The call requires the steady phase, and /// allocates nothing. pub fn poll(self: *Storage) Storage.Exhaustion!Poll { std.debug.assert(self.phase == .steady); std.debug.assert(self.start <= self.end); std.debug.assert(self.end <= self.bytes.len); if (self.terminal) return error.ReaderMessageCapacityExceeded; const buffered = self.bytes[self.start..self.end]; if (std.mem.indexOfScalar(u8, buffered, '\n')) |newline| { self.observe(newline); const line = buffered[0..newline]; self.start += newline + 1; return .{ .line = line }; } self.observe(@min(buffered.len, self.capacity.message_bytes)); if (buffered.len > self.capacity.message_bytes) return self.reject(); if (self.eof) { if (buffered.len == 0) return .end; self.start = self.end; return .{ .line = buffered }; } return .need_input; } /// A caller uses this function to get the free space the caller's next read goes into. The /// function returns the free space at the end of the buffer. The call first moves the unread /// bytes to the front of the buffer, and lines returned earlier stop being valid. The caller /// calls it after `poll` returned `need_input`, and never after `finish` or once the reader is /// terminal. The space the function returns then holds at least one byte. pub fn writable(self: *Storage) []u8 { std.debug.assert(self.phase == .steady); std.debug.assert(!self.eof); std.debug.assert(!self.terminal); std.debug.assert(self.start <= self.end); std.debug.assert(self.end <= self.bytes.len); if (self.start != 0) { const remaining = self.end - self.start; std.mem.copyForwards(u8, self.bytes[0..remaining], self.bytes[self.start..self.end]); self.start = 0; self.end = remaining; } std.debug.assert(self.end < self.bytes.len); return self.bytes[self.end..]; } /// A caller uses this function to hand the bytes that arrived from its read to the reader. The /// function marks `count` bytes at the start of the space from `writable` as read in. The value /// `count` is above zero and at most the size of that space. A read that returns zero bytes /// means the end of input, and the caller calls `finish` for it. pub fn commit(self: *Storage, count: usize) void { std.debug.assert(self.phase == .steady); std.debug.assert(!self.eof); std.debug.assert(!self.terminal); std.debug.assert(count > 0); std.debug.assert(self.start <= self.end); std.debug.assert(count <= self.bytes.len - self.end); self.end += count; std.debug.assert(self.end <= self.bytes.len); } /// A caller uses this function to tell the reader the input has ended. The function marks the /// end of input, so `poll` returns the remaining bytes as a last line and then `end`. The /// caller calls it once, and never once the reader is terminal. pub fn finish(self: *Storage) void { std.debug.assert(self.phase == .steady); std.debug.assert(!self.eof); std.debug.assert(!self.terminal); std.debug.assert(self.start <= self.end); self.eof = true; } /// A caller uses this function to read the reader's sizes and counters at any time. The /// function returns a snapshot of the phase, the sizes and the counters. The call allocates /// nothing and changes nothing. pub fn status(self: *const Storage) Status { return .{ .phase = self.phase, .message_bytes = self.capacity.message_bytes, .storage_bytes = self.capacity.storage_bytes, .buffered_bytes = self.end - self.start, .high_water_message_bytes = self.high_water_message_bytes, .rejected_message_count = self.rejected_message_count, .terminal = self.terminal, }; } /// A caller uses this function to free the buffer when the caller is done reading. The function /// frees the buffer with the allocator given to `init` and moves the reader to its teardown /// phase. The call is allowed before `activate`, as the client does when its setup fails. The /// caller calls it once. pub fn deinit(self: *Storage, allocator: std.mem.Allocator) void { std.debug.assert(self.phase != .teardown); std.debug.assert(self.bytes.len == self.capacity.storage_bytes); self.phase = .teardown; allocator.free(self.bytes); self.bytes = &.{}; self.start = 0; self.end = 0; } fn observe(self: *Storage, message_bytes: usize) void { std.debug.assert(message_bytes <= self.capacity.message_bytes); self.high_water_message_bytes = @max(self.high_water_message_bytes, message_bytes); } fn reject(self: *Storage) Storage.Exhaustion { std.debug.assert(!self.terminal); std.debug.assert(self.end - self.start > self.capacity.message_bytes); self.terminal = true; self.rejected_message_count +|= 1; return error.ReaderMessageCapacityExceeded; }};Source: lib/acp/src/root.zig:91
zig
pub const ReaderStorage = reader.Storage;Also reachable as
Audit
| Definitions | 13 |
|---|---|
| Public names | 26 |
| Members | 9 |
| Version | 26.7.0 |
| Revision | daab053ee433 |