Skip to documentation
SLOP

tiny.acp.ReaderStorage

Reference 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.

Types and contracts

Public types and contracts.

Fields and members

Public fields and members.

No direct callersNo direct callstiny.acpReaderStorage
Static calls · unresolved targets: unknown · external targets: unknown.

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;
Called byCallsNo direct callstest sourcelib.acp.src.reader.storagetest: ACP reader accepts the exact li...test sourcelib.acp.src.reader.storagetest: ACP reader frames fragmented an...test sourcelib.acp.src.reader.storagetest: ACP reader rejection telemetry ...test sourcelib.acp.src.reader.storagetest: ACP reader storage acquires one...ReaderStorageactivate
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.acp.src.reader.storagecheckInitFailurestest sourcelib.acp.src.reader.storagetest: ACP reader accepts the exact li...test sourcelib.acp.src.reader.storagetest: ACP reader frames fragmented an...test sourcelib.acp.src.reader.storagetest: ACP reader rejection telemetry ...test sourcelib.acp.src.reader.storagetest: ACP reader storage acquires one...ReaderStoragedeinit
Static calls · unresolved targets: 0 · external targets: 1.
Called byCallsNo direct callstest sourcelib.acp.src.reader.storagetest: ACP reader frames fragmented an...ReaderStoragefinish
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callsprivate sourcelib.acp.src.reader.storagecheckInitFailurestest sourcelib.acp.src.reader.storagetest: ACP reader accepts the exact li...test sourcelib.acp.src.reader.storagetest: ACP reader frames fragmented an...test sourcelib.acp.src.reader.storagetest: ACP reader framing remains allo...test sourcelib.acp.src.reader.storagetest: ACP reader rejection telemetry ...test sourcelib.acp.src.reader.storagetest: ACP reader storage acquires one...ReaderStorageinit
Static calls · unresolved targets: 1 · external targets: 1.
Called byCallstest sourcelib.acp.src.reader.storagetest: ACP reader accepts the exact li...test sourcelib.acp.src.reader.storagetest: ACP reader frames fragmented an...test sourcelib.acp.src.reader.storagetest: ACP reader rejection telemetry ...private sourcelib.acp.src.reader.storage.Storageobserveprivate sourcelib.acp.src.reader.storage.StoragerejectReaderStoragepoll
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest sourcelib.acp.src.reader.storagetest: ACP reader accepts the exact li...test sourcelib.acp.src.reader.storagetest: ACP reader rejection telemetry ...ReaderStoragestatus
Static calls · unresolved targets: 0 · external targets: 0.
Called byCallsNo direct callstest sourcelib.acp.src.reader.storagetest: ACP reader storage acquires one...ReaderStoragewritable
Static calls · unresolved targets: 0 · external targets: 0.

Also reachable as

reader.Storage.

Audit

Definitions13
Public names26
Members9
Version26.7.0
Revisiondaab053ee433