Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575const std = @import("std");const types = @import("../shared/types.zig");
pub const ToolLifecyclePhase = enum { provisional, authoritative, terminal,};
pub const CancellationPresentation = enum { none, pending, tool_status, replaced,};
const TurnCancellationState = struct { turn_id: u64, interrupted: bool = false, notice_presented: bool = false,};
pub const ToolPresentationRecord = struct { id: types.ToolLifecycleId, entry_id: u32, tool_name: ?[]const u8, activity_kind: types.ToolActivityKind, captured_command: bool = false, cancellation_presentation: CancellationPresentation = .none, 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, turn_cancellation: ?TurnCancellationState = null, 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 needsTurnCancellationNoticeAfterTerminal( self: *const ToolActivityState, id: types.ToolLifecycleId, ) bool { const cancellation = self.turn_cancellation orelse return false; if (cancellation.turn_id != id.turn_id or !cancellation.interrupted or cancellation.notice_presented) { return false; } const settling = self.record(id) orelse return false; switch (settling.cancellation_presentation) { .none => return false, .pending, .tool_status, .replaced => {}, } var iterator = self.records.valueIterator(); while (iterator.next()) |record_ptr| { if (record_ptr.id.turn_id != id.turn_id or std.mem.eql(u8, record_ptr.id.call_id, id.call_id)) { continue; } switch (record_ptr.cancellation_presentation) { .pending, .tool_status => return false, .none, .replaced => {}, } } return true; }
pub fn needsTurnCancellationNoticeAfterTurnFinished( self: *const ToolActivityState, turn_id: u64, ) bool { const cancellation = self.turn_cancellation orelse return false; if (cancellation.turn_id != turn_id or cancellation.notice_presented) { return false; } var iterator = self.records.valueIterator(); while (iterator.next()) |record_ptr| { if (record_ptr.id.turn_id != turn_id) continue; switch (record_ptr.cancellation_presentation) { .pending, .tool_status => return false, .none, .replaced => {}, } } return true; }
pub fn markTurnCancellationPresented( self: *ToolActivityState, turn_id: u64, ) void { self.turn_cancellation = .{ .turn_id = turn_id }; }
pub fn markTurnInterrupted( self: *ToolActivityState, turn_id: u64, ) void { if (self.turn_cancellation) |*cancellation| { if (cancellation.turn_id == turn_id) cancellation.interrupted = true; } }
pub fn markTurnCancellationNoticePresented( self: *ToolActivityState, turn_id: u64, ) void { if (self.turn_cancellation) |*cancellation| { if (cancellation.turn_id == turn_id) cancellation.notice_presented = true; } }
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, .cancellation_presentation = if (self.turn_cancellation) |cancellation| if (cancellation.turn_id == id.turn_id) .pending else .none else .none, .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());}