480 lines · 14.6 KB · Zig
RawDownload
const std = @import("std");const types = @import("../shared/types.zig"); pub const ToolLifecyclePhase = enum {    provisional,    authoritative,    terminal,}; pub const ToolPresentationRecord = struct {    id: types.ToolLifecycleId,    entry_id: u32,    tool_name: ?[]const u8,    activity_kind: types.ToolActivityKind,    captured_command: bool = false,    phase: ToolLifecyclePhase,    focus_seq: u64,}; pub const ActivityProjection = union(enum) {    pub const Tone = enum {        thinking,        neutral,        warning,        success,        danger,    };     pub const TurnThinking = struct {        label: []const u8,        tone: Tone = .thinking,    };     pub const ToolSlot = struct {        entry_id: u32,        fallback_label: []const u8,        active: bool,        kind: types.ToolActivityKind,        thinking_label: ?[]const u8 = null,    };     none,    turn_thinking: TurnThinking,    tool_slot: ToolSlot,}; /// Immutable view of lifecycle state. Slice fields borrow presenter-owned/// storage and remain valid only until the next lifecycle mutation.pub const LifecycleSnapshot = struct {    finalized_turn_watermark: u64,    active_tool_count: usize,    focused_entry_id: ?u32,    focused_activity_kind: ?types.ToolActivityKind,    activity: ActivityProjection,}; /// Result of one lifecycle mutation. Any slices in the terminal record share/// the snapshot's borrowed lifetime.pub const LifecycleTransition = struct {    previous_focused_entry_id: ?u32,    snapshot: LifecycleSnapshot,    applied_activity_kind: ?types.ToolActivityKind,    terminal_record: ?ToolPresentationRecord,     pub fn focus_changed(self: LifecycleTransition) bool {        return self.previous_focused_entry_id != self.snapshot.focused_entry_id;    }}; pub const LifecyclePresenter = struct {    ctx: *anyopaque,    snapshot_fn: *const fn (*anyopaque) LifecycleSnapshot,    record_fn: *const fn (*anyopaque, types.ToolLifecycleId) ?ToolPresentationRecord,    apply_fn: *const fn (*anyopaque, std.mem.Allocator, types.ToolLifecycleEvent) anyerror!LifecycleTransition,    finish_batch_fn: *const fn (*anyopaque, std.mem.Allocator) anyerror!void,     pub fn snapshot(self: LifecyclePresenter) LifecycleSnapshot {        return self.snapshot_fn(self.ctx);    }     pub fn record(        self: LifecyclePresenter,        id: types.ToolLifecycleId,    ) ?ToolPresentationRecord {        return self.record_fn(self.ctx, id);    }     pub fn apply(        self: LifecyclePresenter,        alloc: std.mem.Allocator,        event: types.ToolLifecycleEvent,    ) !LifecycleTransition {        return self.apply_fn(            self.ctx,            alloc,            event,        );    }     pub fn finish_batch(        self: LifecyclePresenter,        alloc: std.mem.Allocator,    ) !void {        return self.finish_batch_fn(self.ctx, alloc);    }}; pub const FallbackRecord = struct {    record: *ToolPresentationRecord,    was_provisional: bool,}; pub const StartAdmission = union(enum) {    accepted,    invalid_identity,    finalized: u64,}; const LifecycleContext = struct {    pub fn hash(_: LifecycleContext, id: types.ToolLifecycleId) u64 {        return std.hash.Wyhash.hash(id.turn_id, id.call_id);    }     pub fn eql(_: LifecycleContext, a: types.ToolLifecycleId, b: types.ToolLifecycleId) bool {        return a.turn_id == b.turn_id and std.mem.eql(u8, a.call_id, b.call_id);    }}; const RecordMap = std.HashMapUnmanaged(    types.ToolLifecycleId,    ToolPresentationRecord,    LifecycleContext,    std.hash_map.default_max_load_percentage,); pub const ToolActivityState = struct {    records: RecordMap = .empty,    batch_finalized_turn_watermark: ?u64 = null,    finalized_turn_watermark: u64 = 0,    next_focus_seq: u64 = 0,     pub fn deinit(self: *ToolActivityState, alloc: std.mem.Allocator) void {        var iterator = self.records.valueIterator();        while (iterator.next()) |record_ptr| freeRecord(alloc, record_ptr.*);        self.records.deinit(alloc);        self.* = .{};    }     pub fn ensureRecordCapacity(        self: *ToolActivityState,        alloc: std.mem.Allocator,    ) !void {        try self.records.ensureUnusedCapacityContext(alloc, 1, .{});    }     pub fn record(        self: *const ToolActivityState,        id: types.ToolLifecycleId,    ) ?ToolPresentationRecord {        return self.records.getContext(id, .{});    }     pub fn recordPtr(        self: *ToolActivityState,        id: types.ToolLifecycleId,    ) ?*ToolPresentationRecord {        return self.records.getPtrContext(id, .{});    }     pub fn recordCount(self: *const ToolActivityState) usize {        return self.records.count();    }     pub fn activeCount(self: *const ToolActivityState) usize {        var count: usize = 0;        var iterator = self.records.valueIterator();        while (iterator.next()) |record_ptr| {            if (record_ptr.phase != .terminal) count += 1;        }        return count;    }     pub fn focusedRecord(        self: *const ToolActivityState,    ) ?*const ToolPresentationRecord {        var focused: ?*const ToolPresentationRecord = null;        var iterator = self.records.valueIterator();        while (iterator.next()) |record_ptr| {            if (record_ptr.phase == .terminal) continue;            if (focused == null or record_ptr.focus_seq > focused.?.focus_seq) {                focused = record_ptr;            }        }        return focused;    }     pub fn admitStart(        self: *const ToolActivityState,        id: types.ToolLifecycleId,    ) StartAdmission {        if (id.turn_id == 0 or id.call_id.len == 0) return .invalid_identity;        const fence = @max(            self.finalized_turn_watermark,            self.batch_finalized_turn_watermark orelse 0,        );        if (id.turn_id <= fence) return .{ .finalized = fence };        return .accepted;    }     pub fn isFenced(        self: *const ToolActivityState,        turn_id: u64,    ) bool {        return turn_id <= @max(            self.finalized_turn_watermark,            self.batch_finalized_turn_watermark orelse 0,        );    }     pub fn putOwnedRecord(        self: *ToolActivityState,        id: types.ToolLifecycleId,        tool_name: ?[]const u8,        activity_kind: types.ToolActivityKind,        entry_id: u32,        phase: ToolLifecyclePhase,    ) void {        const focus_seq = self.claimFocus();        self.records.putAssumeCapacityNoClobberContext(id, .{            .id = id,            .entry_id = entry_id,            .tool_name = tool_name,            .activity_kind = activity_kind,            .phase = phase,            .focus_seq = focus_seq,        }, .{});    }     pub fn promoteOwned(        self: *ToolActivityState,        alloc: std.mem.Allocator,        record_ptr: *ToolPresentationRecord,        tool_name: []const u8,        activity_kind: types.ToolActivityKind,    ) bool {        const first_authoritative = record_ptr.phase != .authoritative;        replaceToolName(alloc, record_ptr, tool_name);        record_ptr.activity_kind = activity_kind;        record_ptr.phase = .authoritative;        if (first_authoritative) record_ptr.focus_seq = self.claimFocus();        return first_authoritative;    }     pub fn rekeyOwned(        self: *ToolActivityState,        alloc: std.mem.Allocator,        old_id: types.ToolLifecycleId,        new_id: types.ToolLifecycleId,        tool_name: []const u8,        activity_kind: types.ToolActivityKind,    ) void {        var removed = self.records.fetchRemoveContext(old_id, .{}).?;        alloc.free(@constCast(removed.value.id.call_id));        if (removed.value.tool_name) |old_name| alloc.free(@constCast(old_name));        removed.value.id = new_id;        removed.value.tool_name = tool_name;        removed.value.activity_kind = activity_kind;        removed.value.phase = .authoritative;        removed.value.focus_seq = self.claimFocus();        self.records.putAssumeCapacityNoClobberContext(new_id, removed.value, .{});    }     pub fn shouldApplyTurnFinished(        self: *const ToolActivityState,        turn_id: u64,    ) bool {        if (turn_id == 0) return false;        if (turn_id <= self.finalized_turn_watermark) return false;        if (self.batch_finalized_turn_watermark) |watermark| {            if (turn_id <= watermark) return false;        }        return true;    }     pub fn collectFallbackRecords(        self: *ToolActivityState,        alloc: std.mem.Allocator,        turn_id: u64,    ) ![]FallbackRecord {        var count: usize = 0;        var count_iterator = self.records.valueIterator();        while (count_iterator.next()) |record_ptr| {            if (record_ptr.id.turn_id == turn_id and record_ptr.phase != .terminal) {                count += 1;            }        }         const result = try alloc.alloc(FallbackRecord, count);        var index: usize = 0;        var iterator = self.records.valueIterator();        while (iterator.next()) |record_ptr| {            if (record_ptr.id.turn_id != turn_id or record_ptr.phase == .terminal) continue;            result[index] = .{                .record = record_ptr,                .was_provisional = record_ptr.phase == .provisional,            };            index += 1;        }        return result;    }     pub fn commitTurnFinished(        self: *ToolActivityState,        turn_id: u64,        fallbacks: []const FallbackRecord,    ) void {        for (fallbacks) |fallback| {            fallback.record.phase = .terminal;        }        self.batch_finalized_turn_watermark = @max(            self.batch_finalized_turn_watermark orelse 0,            turn_id,        );    }     pub fn cleanupEntryIds(        self: *const ToolActivityState,        alloc: std.mem.Allocator,        watermark: u64,    ) ![]u32 {        var count: usize = 0;        var count_iterator = self.records.valueIterator();        while (count_iterator.next()) |record_ptr| {            if (record_ptr.id.turn_id <= watermark) count += 1;        }         const result = try alloc.alloc(u32, count);        var index: usize = 0;        var iterator = self.records.valueIterator();        while (iterator.next()) |record_ptr| {            if (record_ptr.id.turn_id > watermark) continue;            result[index] = record_ptr.entry_id;            index += 1;        }        return result;    }     pub fn pausedTurnEntryIds(        self: *const ToolActivityState,        alloc: std.mem.Allocator,        turn_id: u64,    ) ![]u32 {        var count: usize = 0;        var count_iterator = self.records.valueIterator();        while (count_iterator.next()) |record_ptr| {            if (record_ptr.id.turn_id == turn_id) count += 1;        }         const result = try alloc.alloc(u32, count);        var index: usize = 0;        var iterator = self.records.valueIterator();        while (iterator.next()) |record_ptr| {            if (record_ptr.id.turn_id != turn_id) continue;            result[index] = record_ptr.entry_id;            index += 1;        }        return result;    }     pub fn releasePausedTurn(        self: *ToolActivityState,        alloc: std.mem.Allocator,        turn_id: u64,    ) void {        while (true) {            var removable: ?types.ToolLifecycleId = null;            var iterator = self.records.iterator();            while (iterator.next()) |entry| {                if (entry.value_ptr.id.turn_id == turn_id) {                    removable = entry.key_ptr.*;                    break;                }            }            const id = removable orelse break;            const removed = self.records.fetchRemoveContext(id, .{}).?;            freeRecord(alloc, removed.value);        }    }     pub fn commitCleanup(        self: *ToolActivityState,        alloc: std.mem.Allocator,        watermark: u64,    ) void {        while (true) {            var removable: ?types.ToolLifecycleId = null;            var iterator = self.records.iterator();            while (iterator.next()) |entry| {                if (entry.value_ptr.id.turn_id <= watermark) {                    removable = entry.key_ptr.*;                    break;                }            }            const id = removable orelse break;            const removed = self.records.fetchRemoveContext(id, .{}).?;            freeRecord(alloc, removed.value);        }        self.finalized_turn_watermark = @max(            self.finalized_turn_watermark,            watermark,        );        self.batch_finalized_turn_watermark = null;    }     fn claimFocus(self: *ToolActivityState) u64 {        self.next_focus_seq +%= 1;        if (self.next_focus_seq == 0) self.next_focus_seq = 1;        return self.next_focus_seq;    }}; pub fn replaceToolName(    alloc: std.mem.Allocator,    record: *ToolPresentationRecord,    owned_tool_name: []const u8,) void {    if (record.tool_name) |old_name| alloc.free(@constCast(old_name));    record.tool_name = owned_tool_name;} fn freeRecord(    alloc: std.mem.Allocator,    record: ToolPresentationRecord,) void {    alloc.free(@constCast(record.id.call_id));    if (record.tool_name) |tool_name| alloc.free(@constCast(tool_name));} test "tool activity start admission describes invalid and finalized identities" {    var state = ToolActivityState{};     try std.testing.expectEqual(        StartAdmission.invalid_identity,        state.admitStart(.{ .turn_id = 0, .call_id = "call" }),    );    try std.testing.expectEqual(        StartAdmission.invalid_identity,        state.admitStart(.{ .turn_id = 1, .call_id = "" }),    );    try std.testing.expectEqual(        StartAdmission.accepted,        state.admitStart(.{ .turn_id = 1, .call_id = "call" }),    );     state.commitCleanup(std.testing.allocator, 1);    switch (state.admitStart(.{ .turn_id = 1, .call_id = "call" })) {        .finalized => |fence| try std.testing.expectEqual(@as(u64, 1), fence),        .accepted, .invalid_identity => return error.TestUnexpectedResult,    }} test "lifecycle transition reports focus changes from immutable snapshots" {    const unchanged = LifecycleTransition{        .previous_focused_entry_id = 4,        .snapshot = .{            .finalized_turn_watermark = 1,            .active_tool_count = 1,            .focused_entry_id = 4,            .focused_activity_kind = .read,            .activity = .none,        },        .applied_activity_kind = .read,        .terminal_record = null,    };    try std.testing.expect(!unchanged.focus_changed());     var changed = unchanged;    changed.snapshot.focused_entry_id = 7;    try std.testing.expect(changed.focus_changed());}