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());}
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());
}