const std = @import("std"); const debug_trace = @import("../shared/debug_trace.zig"); const io_mod = @import("../shared/io.zig"); const message = @import("../shared/message.zig"); const text_utils = @import("../shared/text_utils.zig"); const types = @import("../shared/types.zig"); const Allocator = std.mem.Allocator; const ToolCall = types.ToolCall; pub fn makePersistedToolResult( alloc: Allocator, tool_call_id_src: []const u8, tool_name_src: []const u8, status: types.PersistedToolStatus, output_src: []const u8, memory: ?types.ToolResultMemory, ) !types.PersistedToolResult { const tool_call_id = try durableIdentifier(alloc, tool_call_id_src); errdefer alloc.free(tool_call_id); const tool_name = try alloc.dupe(u8, tool_name_src); errdefer alloc.free(tool_name); const output = try alloc.dupe(u8, output_src); errdefer alloc.free(output); const output_handle = if (memory) |info| if (info.output_handle) |handle| try alloc.dupe(u8, handle) else null else null; errdefer if (output_handle) |handle| alloc.free(handle); const preview = if (memory) |info| if (info.preview) |text| try alloc.dupe(u8, text) else null else null; errdefer if (preview) |text| alloc.free(text); const command_output_replay = if (memory) |info| if (info.command_output_replay) |replay| try types.dupeCommandOutputReplay(alloc, replay) else null else null; errdefer if (command_output_replay) |replay| types.freeCommandOutputReplay(alloc, replay); const tool_image_handle = if (memory) |info| if (info.tool_image_handle) |handle| try alloc.dupe(u8, handle) else null else null; errdefer if (tool_image_handle) |handle| alloc.free(handle); const tool_images = if (memory) |info| if (tool_image_handle == null) try types.dupeToolImages(alloc, info.tool_images) else try alloc.alloc(types.ToolImage, 0) else try alloc.alloc(types.ToolImage, 0); errdefer types.freeToolImages(alloc, tool_images); var result: types.PersistedToolResult = .{ .tool_images = tool_images, .tool_image_handle = tool_image_handle, .tool_call_id = tool_call_id, .tool_name = tool_name, .status = status, .output = output, .output_handle = output_handle, .preview = preview, .output_bytes = if (memory) |info| info.output_bytes else output_src.len, .stored_output_bytes = if (memory) |info| info.stored_output_bytes else output.len, .truncated = if (memory) |info| info.truncated else false, .provider_native = false, .review_feedback = if (memory) |info| info.review_feedback else false, .created_at_ms = io_mod.milliTimestamp(), .command_output_replay = command_output_replay, .command_process_presentation = if (memory) |info| info.command_process_presentation else null, .terminal_action_presentation = if (memory) |info| info.terminal_action_presentation else null, }; if (memory) |info| { if (info.committed_file_presentation) |presentation| { result.committed_file_presentation = dupeCommittedFilePresentation( alloc, presentation, ) catch |err| blk: { debug_trace.logf( "session", "committed file resume presentation unavailable call_id={s} err={s}", .{ tool_call_id_src, @errorName(err) }, ); break :blk null; }; } } return result; } pub fn buildNormalChatExecutionMemory( alloc: Allocator, messages: []const types.ChatMessage, ) !types.ExecutionMemory { return buildNormalExecutionMemory(ChatMessageAdapter, alloc, messages); } test "completed tool exchange retains provider replay with its assistant" { const alloc = std.testing.allocator; const state = types.ProviderReplay{ .source = .{ .provider = .gateway, .model = "test/model" }, .parts_json = "[{\"type\":\"reasoning\",\"text\":\"\",\"providerOptions\":{\"openai\":{\"reasoningEncryptedContent\":\"opaque\"}}}]", }; const calls = [_]ToolCall{.{ .id = "read", .name = "read_file", .arguments_json = "{ \"path\": \"file\" }" }}; const messages = [_]types.ChatMessage{ .{ .role = .assistant, .tool_calls = &calls, .provider_replay = state }, .{ .role = .tool, .tool_call_id = "read", .tool_name = "read_file", .content = "contents", .tool_result_status = .success }, }; const execution = try buildNormalChatExecutionMemory(alloc, &messages); defer types.freeExecutionMemory(alloc, execution); try std.testing.expectEqual(@as(usize, 1), execution.tool_steps.len); const stored = execution.tool_steps[0].provider_replay orelse return error.TestExpectedProviderReplay; try std.testing.expectEqualStrings(state.parts_json, stored.parts_json); try std.testing.expectEqualStrings(state.source.model, stored.source.model); try std.testing.expect(stored.parts_json.ptr != state.parts_json.ptr); } pub fn buildNormalMessageExecutionMemory( alloc: Allocator, messages: []const message.Message, ) !types.ExecutionMemory { return buildNormalExecutionMemory(MessageAdapter, alloc, messages); } pub fn dupeCompletedPersistedToolCalls( alloc: Allocator, calls: []const ToolCall, results: []const types.PersistedToolResult, ) ![]ToolCall { if (calls.len == 0 or results.len == 0) return &.{}; var copy: std.ArrayList(ToolCall) = .empty; errdefer { for (copy.items) |call| types.freeToolCall(alloc, call); copy.deinit(alloc); } for (calls) |call| { const completed = blk: { const durable_id = try durableIdentifier(alloc, call.id); defer alloc.free(durable_id); break :blk hasPersistedResultForCall(results, durable_id); }; if (!completed) continue; const duplicated = try dupePersistedToolCall(alloc, call); copy.append(alloc, duplicated) catch |err| { types.freeToolCall(alloc, duplicated); return err; }; } return copy.toOwnedSlice(alloc); } /// Display-only projection: masks secret-shaped spans and returns an owned /// copy. Persisted history must use verbatim copies instead. pub fn maskTextForDisplay(alloc: Allocator, text: []const u8) ![]u8 { const masked = text_utils.maskSecrets(alloc, text) catch return error.OutOfMemory; if (masked.ptr == text.ptr) return alloc.dupe(u8, text); return @constCast(masked); } fn dupeCommittedFilePresentation( alloc: Allocator, presentation: types.CommittedFilePresentation, ) !types.CommittedFilePresentation { const path = try alloc.dupe(u8, presentation.path); errdefer alloc.free(path); const lines = try alloc.alloc(types.CommittedFilePresentationLine, presentation.lines.len); errdefer alloc.free(lines); var copied_lines: usize = 0; errdefer { for (lines[0..copied_lines]) |line| alloc.free(@constCast(line.text)); } for (presentation.lines, 0..) |line, index| { lines[index] = .{ .kind = line.kind, .old_line = line.old_line, .new_line = line.new_line, .text = try alloc.dupe(u8, line.text), }; copied_lines += 1; } const previous_content = if (presentation.previous_content) |content| try alloc.dupe(u8, content) else null; errdefer if (previous_content) |content| alloc.free(content); const after_content = if (presentation.after_content) |content| try alloc.dupe(u8, content) else null; errdefer if (after_content) |content| alloc.free(content); const lifecycle_id: ?types.ToolLifecycleId = if (presentation.lifecycle_id) |id| .{ .turn_id = id.turn_id, .call_id = try durableIdentifier(alloc, id.call_id), } else null; errdefer if (lifecycle_id) |id| alloc.free(@constCast(id.call_id)); return .{ .path = path, .kind = presentation.kind, .lines = lines, .additions = presentation.additions, .deletions = presentation.deletions, .truncated = presentation.truncated, .previous_content = previous_content, .after_content = after_content, .lifecycle_id = lifecycle_id, }; } pub fn appendFileEvidenceForTool( alloc: Allocator, files: *std.ArrayList(types.FileEvidence), call: ToolCall, status: types.PersistedToolStatus, memory: ?types.ToolResultMemory, ) !void { const action = fileEvidenceActionForTool(call.name); if (action == .unknown) return; var parsed = std.json.parseFromSlice(std.json.Value, alloc, call.arguments_json, .{}) catch return; defer parsed.deinit(); if (parsed.value != .object) return; const object = parsed.value.object; const path = switch (action) { .search => non_empty_string_field(object, "path") orelse non_empty_string_field(object, "directory") orelse non_empty_string_field(object, "query") orelse ".", else => non_empty_string_field(object, "path") orelse return, }; const evidence = try makeFileEvidence( alloc, call, path, action, status, memory, ); var owns_evidence = true; errdefer if (owns_evidence) freeTransientFileEvidence(alloc, evidence); try files.append(alloc, evidence); owns_evidence = false; } pub fn markStaleFileEvidence(files: []types.FileEvidence) void { for (files, 0..) |file, i| { if (file.status != .success or !isMutationFileAction(file.action)) continue; var prior_index: usize = 0; while (prior_index < i) : (prior_index += 1) { if (files[prior_index].action != .read) continue; if (std.mem.eql(u8, files[prior_index].path, file.path)) { files[prior_index].stale = true; } } } } pub fn findToolCallById(calls: []const ToolCall, id: []const u8) ?ToolCall { for (calls) |call| { if (std.mem.eql(u8, call.id, id)) return call; } return null; } const ChatMessageAdapter = struct { fn standalone(value: Message) bool { return value.standalone_response or value.provider_replay != null; } pub const Message = types.ChatMessage; fn providerReplay(value: Message) ?types.ProviderReplay { return value.provider_replay; } fn isAssistant(value: Message) bool { return value.role == .assistant; } fn isTool(value: Message) bool { return value.role == .tool; } fn isUser(value: Message) bool { return value.role == .user; } fn isPermissionFeedback(value: Message) bool { return value.role == .user and value.permission_feedback; } fn toolCalls(value: Message) []const ToolCall { return value.tool_calls; } fn toolCallId(value: Message) ?[]const u8 { return value.tool_call_id; } fn toolName(value: Message) ?[]const u8 { return value.tool_name; } fn toolResultStatus(value: Message) ?types.PersistedToolStatus { return value.tool_result_status; } fn toolResultMemory(value: Message) ?types.ToolResultMemory { return value.tool_result_memory; } fn content(value: Message) ?[]const u8 { return value.content; } }; const MessageAdapter = struct { fn standalone(_: Message) bool { return false; } pub const Message = message.Message; fn providerReplay(_: Message) ?types.ProviderReplay { return null; } fn isAssistant(value: Message) bool { return value.role == .assistant; } fn isTool(value: Message) bool { return value.role == .tool; } fn isUser(value: Message) bool { return value.role == .user; } fn isPermissionFeedback(value: Message) bool { return value.role == .user and value.permission_feedback; } fn toolCalls(value: Message) []const ToolCall { return value.tool_calls; } fn toolCallId(value: Message) ?[]const u8 { return value.tool_call_id; } fn toolName(value: Message) ?[]const u8 { return value.tool_name; } fn toolResultStatus(value: Message) ?types.PersistedToolStatus { return value.tool_result_status; } fn toolResultMemory(value: Message) ?types.ToolResultMemory { return value.tool_result_memory; } fn content(value: Message) ?[]const u8 { return if (value.content) |content_value| content_value.asText() else null; } }; fn buildNormalExecutionMemory( comptime Adapter: type, alloc: Allocator, messages: []const Adapter.Message, ) !types.ExecutionMemory { var tool_steps: std.ArrayList(types.ToolExecutionStep) = .empty; errdefer { for (tool_steps.items) |step| { freeTransientToolExecutionStep(alloc, step); } tool_steps.deinit(alloc); } var files: std.ArrayList(types.FileEvidence) = .empty; errdefer { for (files.items) |file| { freeTransientFileEvidence(alloc, file); } files.deinit(alloc); } var i: usize = 0; while (i < messages.len) { const msg = messages[i]; const tool_calls = Adapter.toolCalls(msg); if (!Adapter.isAssistant(msg)) { i += 1; continue; } if (tool_calls.len == 0) { if (Adapter.standalone(msg)) { const assistant = if (Adapter.content(msg)) |content| try alloc.dupe(u8, content) else null; errdefer if (assistant) |text| alloc.free(text); const replay = try dupeUnchangedProviderReplay(alloc, Adapter.providerReplay(msg), Adapter.content(msg), assistant, &.{}, &.{}); errdefer if (replay) |value| types.freeProviderReplay(alloc, value); try tool_steps.append(alloc, .{ .assistant = assistant, .provider_replay = replay }); } i += 1; continue; } var results: std.ArrayList(types.PersistedToolResult) = .empty; errdefer { for (results.items) |result| { freeTransientPersistedToolResult(alloc, result); } results.deinit(alloc); } var result_source_call_ids: std.ArrayList([]const u8) = .empty; defer result_source_call_ids.deinit(alloc); var j = i + 1; var last_result_index: ?usize = null; while (j < messages.len and Adapter.isTool(messages[j])) : (j += 1) { const result_msg = messages[j]; const tool_call_id = Adapter.toolCallId(result_msg) orelse continue; const call = findToolCallById(tool_calls, tool_call_id) orelse continue; const output = Adapter.content(result_msg) orelse ""; const status = Adapter.toolResultStatus(result_msg) orelse return error.MissingToolResultStatus; const result_memory = Adapter.toolResultMemory(result_msg); const record = try makePersistedToolResult( alloc, tool_call_id, Adapter.toolName(result_msg) orelse call.name, status, output, result_memory, ); var owns_record = true; errdefer if (owns_record) { freeTransientPersistedToolResult(alloc, record); }; try results.append(alloc, record); owns_record = false; try result_source_call_ids.append(alloc, tool_call_id); last_result_index = results.items.len - 1; try appendFileEvidenceForTool( alloc, &files, call, status, result_memory, ); } while (j < messages.len and Adapter.isUser(messages[j])) : (j += 1) { const feedback_msg = messages[j]; if (!Adapter.isPermissionFeedback(feedback_msg)) continue; const text = Adapter.content(feedback_msg) orelse return error.MissingPermissionFeedbackContent; const index = if (Adapter.toolCallId(feedback_msg)) |tool_call_id| findResultIndexBySourceCallId(result_source_call_ids.items, tool_call_id) orelse continue else last_result_index orelse return error.PermissionFeedbackWithoutResult; try appendPersistedPermissionFeedback(alloc, &results.items[index], text); } if (results.items.len == 0) { results.deinit(alloc); i = j; continue; } const assistant = if (Adapter.content(msg)) |content| try alloc.dupe(u8, content) else null; errdefer if (assistant) |text| alloc.free(text); const persisted_calls = try dupeCompletedPersistedToolCalls( alloc, tool_calls, results.items, ); errdefer types.freeToolCallSlice(alloc, persisted_calls); const replay = try dupeUnchangedProviderReplay(alloc, Adapter.providerReplay(msg), Adapter.content(msg), assistant, tool_calls, persisted_calls); errdefer if (replay) |value| types.freeProviderReplay(alloc, value); const owned_results = try results.toOwnedSlice(alloc); errdefer types.freePersistedToolResults(alloc, owned_results); const step = types.ToolExecutionStep{ .assistant = assistant, .provider_replay = replay, .tool_calls = persisted_calls, .tool_results = owned_results, }; try tool_steps.append(alloc, step); i = j; } const owned_files = try files.toOwnedSlice(alloc); errdefer types.freeFileEvidenceSlice(alloc, owned_files); markStaleFileEvidence(owned_files); return .{ .tool_steps = try tool_steps.toOwnedSlice(alloc), .files = owned_files, }; } fn hasPersistedResultForCall( results: []const types.PersistedToolResult, call_id: []const u8, ) bool { for (results) |result| { if (std.mem.eql(u8, result.tool_call_id, call_id)) return true; } return false; } fn findResultIndexBySourceCallId( source_call_ids: []const []const u8, call_id: []const u8, ) ?usize { for (source_call_ids, 0..) |source_call_id, index| { if (std.mem.eql(u8, source_call_id, call_id)) return index; } return null; } pub fn freeTransientToolExecutionStep( alloc: Allocator, step: types.ToolExecutionStep, ) void { if (step.assistant) |assistant| alloc.free(assistant); if (step.provider_replay) |replay| types.freeProviderReplay(alloc, replay); types.freeToolCallSlice(alloc, step.tool_calls); types.freePersistedToolResults(alloc, step.tool_results); } /// Caller owns the copy. Never bind signed metadata to incomplete history. pub fn dupeUnchangedProviderReplay( alloc: Allocator, replay: ?types.ProviderReplay, source_text: ?[]const u8, stored_text: ?[]const u8, source_calls: []const ToolCall, stored_calls: []const ToolCall, ) !?types.ProviderReplay { const value = replay orelse return null; var unchanged = std.mem.eql(u8, source_text orelse "", stored_text orelse "") and source_calls.len == stored_calls.len; if (unchanged) for (source_calls, stored_calls) |source, stored| { if (!std.mem.eql(u8, source.id, stored.id) or !std.mem.eql(u8, source.name, stored.name) or !try @import("../shared/json_comparison.zig").serializedEqual(alloc, source.arguments_json, stored.arguments_json)) { unchanged = false; break; } }; if (!unchanged) { debug_trace.logf("session", "provider replay omitted reason=incomplete_association", .{}); return null; } return try types.dupeProviderReplay(alloc, value); } pub fn freeTransientPersistedToolResult( alloc: Allocator, result: types.PersistedToolResult, ) void { alloc.free(result.tool_call_id); alloc.free(result.tool_name); alloc.free(result.output); types.freeToolImages(alloc, result.tool_images); if (result.tool_image_handle) |handle| alloc.free(handle); if (result.output_handle) |handle| alloc.free(handle); if (result.preview) |preview| alloc.free(preview); types.freePermissionFeedback(alloc, result.permission_feedback); if (result.committed_file_presentation) |presentation| { types.freeCommittedFilePresentation(alloc, presentation); } if (result.command_output_replay) |replay| { types.freeCommandOutputReplay(alloc, replay); } } fn appendPersistedPermissionFeedback( alloc: Allocator, result: *types.PersistedToolResult, text: []const u8, ) !void { const owned = try alloc.dupe(u8, text); errdefer alloc.free(owned); const prior = result.permission_feedback; const appended = try alloc.alloc([]u8, prior.len + 1); errdefer alloc.free(appended); @memcpy(appended[0..prior.len], prior); appended[prior.len] = owned; if (prior.len > 0) alloc.free(prior); result.permission_feedback = appended; } pub fn freeTransientFileEvidence( alloc: Allocator, file: types.FileEvidence, ) void { alloc.free(file.path); if (file.new_path) |new_path| alloc.free(new_path); alloc.free(file.tool_call_id); alloc.free(file.tool_name); } pub fn dupePersistedToolCall(alloc: Allocator, call: ToolCall) !ToolCall { const id = try durableIdentifier(alloc, call.id); errdefer alloc.free(id); const name = try alloc.dupe(u8, call.name); errdefer alloc.free(name); const arguments_json = try alloc.dupe(u8, call.arguments_json); errdefer alloc.free(arguments_json); const provisional_id = if (call.provisional_id) |value| try durableIdentifier(alloc, value) else null; errdefer if (provisional_id) |value| alloc.free(value); const provider_result = if (call.provider_result) |result| try alloc.dupe(u8, result) else null; errdefer if (provider_result) |result| alloc.free(result); return .{ .id = id, .name = name, .arguments_json = arguments_json, .provisional_id = provisional_id, .provider_result = provider_result, .final_identity = call.final_identity, .provenance = call.provenance, }; } const ArgumentRedactionPolicy = struct { web_fetch: bool = false, }; /// Client-facing display projection of tool-call arguments (ACP replay). /// Persisted history keeps arguments verbatim; only display surfaces mask. pub fn redactToolArgumentsJson( alloc: Allocator, tool_name: []const u8, arguments_json: []const u8, ) ![]u8 { var parsed = std.json.parseFromSlice(std.json.Value, alloc, arguments_json, .{}) catch { return maskTextForDisplay(alloc, arguments_json); }; defer parsed.deinit(); const policy = ArgumentRedactionPolicy{ .web_fetch = std.mem.eql(u8, tool_name, "web_fetch"), }; var out: std.Io.Writer.Allocating = .init(alloc); errdefer out.deinit(); try writeRedactedJsonValue(alloc, &out.writer, parsed.value, policy, false); return out.toOwnedSlice() catch return error.OutOfMemory; } fn writeRedactedJsonValue( alloc: Allocator, writer: *std.Io.Writer, value: std.json.Value, policy: ArgumentRedactionPolicy, redact_value: bool, ) !void { if (redact_value) { try std.json.Stringify.value("[REDACTED]", .{}, writer); return; } switch (value) { .object => |object| { try writer.writeByte('{'); var iter = object.iterator(); var first = true; while (iter.next()) |entry| { if (!first) try writer.writeByte(','); first = false; try std.json.Stringify.value(entry.key_ptr.*, .{}, writer); try writer.writeByte(':'); if (policy.web_fetch and std.mem.eql(u8, entry.key_ptr.*, "url") and entry.value_ptr.* == .string) { const display_url = try text_utils.redactUrlForDisplay( alloc, entry.value_ptr.string, ); defer alloc.free(display_url); try writeMaskedJsonString(alloc, writer, display_url); } else { try writeRedactedJsonValue( alloc, writer, entry.value_ptr.*, policy, shouldRedactArgumentValue(entry.key_ptr.*), ); } } try writer.writeByte('}'); }, .array => |array| { try writer.writeByte('['); for (array.items, 0..) |item, i| { if (i > 0) try writer.writeByte(','); try writeRedactedJsonValue(alloc, writer, item, policy, false); } try writer.writeByte(']'); }, .string => |text| try writeMaskedJsonString(alloc, writer, text), else => try std.json.Stringify.value(value, .{}, writer), } } fn writeMaskedJsonString( alloc: Allocator, writer: *std.Io.Writer, text: []const u8, ) !void { const masked = text_utils.maskSecrets(alloc, text) catch return error.OutOfMemory; defer if (masked.ptr != text.ptr) alloc.free(@constCast(masked)); try std.json.Stringify.value(masked, .{}, writer); } fn shouldRedactArgumentValue(key: []const u8) bool { if (isCredentialArgumentKey(key)) return true; return false; } fn isCredentialArgumentKey(key: []const u8) bool { const exact = [_][]const u8{ "password", "passwd", "token", "api_key", "apikey", "secret", "secret_key", "secretkey", "client_secret", "clientsecret", "access_token", "accesstoken", "refresh_token", "refreshtoken", "auth_token", "authtoken", "id_token", "idtoken", "private_key", "privatekey", "access_key", "accesskey", "authorization", "cookie", "set_cookie", "setcookie", "credential", "credentials", }; for (exact) |candidate| { if (std.ascii.eqlIgnoreCase(key, candidate)) return true; } return false; } fn makeFileEvidence( alloc: Allocator, call: ToolCall, path_src: []const u8, action: types.FileEvidenceAction, status: types.PersistedToolStatus, memory: ?types.ToolResultMemory, ) !types.FileEvidence { const path = try alloc.dupe(u8, path_src); errdefer alloc.free(path); const tool_call_id = try durableIdentifier(alloc, call.id); errdefer alloc.free(tool_call_id); const tool_name = try alloc.dupe(u8, call.name); errdefer alloc.free(tool_name); const model_view_covers_full_file = if (memory) |info| (info.model_view_covers_full_file orelse false) and !info.truncated else false; return .{ .path = path, .tool_call_id = tool_call_id, .tool_name = tool_name, .action = action, .status = status, .model_view_covers_full_file = status == .success and action == .read and model_view_covers_full_file, .stale = false, }; } fn fileEvidenceActionForTool(tool_name: []const u8) types.FileEvidenceAction { if (std.mem.eql(u8, tool_name, "read_file")) return .read; if (std.mem.eql(u8, tool_name, "write_file")) return .write; if (std.mem.eql(u8, tool_name, "edit_file")) return .edit; if (std.mem.eql(u8, tool_name, "grep_files")) return .search; if (std.mem.eql(u8, tool_name, "glob_files")) return .search; return .unknown; } fn non_empty_string_field(object: std.json.ObjectMap, name: []const u8) ?[]const u8 { const value = object.get(name) orelse return null; if (value != .string or value.string.len == 0) return null; return value.string; } fn isMutationFileAction(action: types.FileEvidenceAction) bool { return switch (action) { .write, .edit => true, else => false, }; } fn durableIdentifier(alloc: Allocator, value: []const u8) ![]u8 { const masked = text_utils.maskSecrets(alloc, value) catch |err| switch (err) { error.OutOfMemory, error.WriteFailed => return error.OutOfMemory, }; if (masked.ptr == value.ptr) return alloc.dupe(u8, value); defer alloc.free(@constCast(masked)); var digest: [32]u8 = undefined; std.crypto.hash.sha2.Sha256.hash(value, &digest, .{}); const hex = std.fmt.bytesToHex(digest[0..12].*, .lower); return std.fmt.allocPrint(alloc, "redacted-{s}", .{&hex}); } test "execution memory persists secret-bearing arguments results and provider output verbatim" { const runtime_execution_memory = @import("runtime/execution_memory.zig"); const ChatMessage = types.ChatMessage; const alloc = std.testing.allocator; const arguments_json = "{\"command\":\"echo ok && AI_GATEWAY_KEY=abcdefghijklmnop\",\"api_key\":\"secret-value-123456\"}"; const provider_result = "provider echoed sk-abcdefghijklmnopqrstuvwxyz123456"; const result_output = "TOOL_DATA_TOKEN=abcdefghijklmnopqrstuvwxyz\nBearer abcdefghijklmnopqrstuvwxyz1234"; var calls = [_]ToolCall{.{ .id = "call_secret", .name = "run_command", .arguments_json = arguments_json, .provider_result = provider_result, }}; const messages = [_]ChatMessage{ .{ .role = .assistant, .tool_calls = calls[0..] }, .{ .role = .tool, .content = result_output, .tool_call_id = "call_secret", .tool_name = "run_command", .tool_result_status = .success }, }; const memory = try runtime_execution_memory.buildExecutionMemory(alloc, &messages); defer types.freeExecutionMemory(alloc, memory); try std.testing.expectEqual(@as(usize, 1), memory.tool_steps.len); const persisted_call = memory.tool_steps[0].tool_calls[0]; const persisted_result = memory.tool_steps[0].tool_results[0]; try std.testing.expectEqualStrings(result_output, persisted_result.output); try std.testing.expectEqualStrings(provider_result, persisted_call.provider_result.?); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, persisted_call.arguments_json, .{}); defer parsed.deinit(); try std.testing.expectEqualStrings("secret-value-123456", parsed.value.object.get("api_key").?.string); } test "execution memory removes token-shaped call ids from JSON" { const runtime_execution_memory = @import("runtime/execution_memory.zig"); const testing_session_json = @import("../session/session_json.zig"); const ChatMessage = types.ChatMessage; const alloc = std.testing.allocator; const secret_id = "sk-abcdefghijklmnop"; var calls = [_]ToolCall{.{ .id = secret_id, .name = "read_file", .arguments_json = "{\"path\":\"README.md\"}", .provisional_id = secret_id, }}; const messages = [_]ChatMessage{ .{ .role = .assistant, .tool_calls = calls[0..] }, .{ .role = .tool, .content = "README", .tool_call_id = secret_id, .tool_name = "read_file", .tool_result_status = .success, .tool_result_memory = .{ .model_view_covers_full_file = true, }, }, }; const memory = try runtime_execution_memory.buildExecutionMemory(alloc, &messages); defer types.freeExecutionMemory(alloc, memory); try std.testing.expectEqualStrings( memory.tool_steps[0].tool_calls[0].id, memory.tool_steps[0].tool_results[0].tool_call_id, ); try std.testing.expectEqualStrings( memory.tool_steps[0].tool_calls[0].id, memory.files[0].tool_call_id, ); var json: std.Io.Writer.Allocating = .init(alloc); defer json.deinit(); try testing_session_json.writeExecutionMemoryJson( &json.writer, memory, ); try std.testing.expect(std.mem.find(u8, json.written(), secret_id) == null); } test "durable execution memory persists token-shaped values verbatim" { const alloc = std.testing.allocator; const arguments_json = \\{"command":"AI_GATEWAY_KEY=abcdefghijklmnop","nested":{"values":["sk-abcdefghijklmnopqrstuvwxyz123456","github_pat_abcdefghijklmnopqrstuvwxyz1234","plain"]},"api_key":"named-secret-123456"} ; const result_output = "API_TOKEN=abcdefghijklmnopqrstuvwxyz\nAuthorization: Bearer abcdefghijklmnopqrstuvwxyz1234"; const preview = "TOKEN=abcdefghijklmnopqrstuvwxyz"; const call = ToolCall{ .id = "call_secret", .name = "mcp__fixture__echo", .arguments_json = arguments_json, }; const result = try makePersistedToolResult( alloc, call.id, call.name, .success, result_output, .{ .preview = preview, .output_bytes = 128, .stored_output_bytes = 64, .truncated = true, }, ); defer freeTransientPersistedToolResult(alloc, result); const calls = [_]ToolCall{call}; const results = [_]types.PersistedToolResult{result}; const persisted_calls = try dupeCompletedPersistedToolCalls(alloc, &calls, &results); defer types.freeToolCallSlice(alloc, persisted_calls); try std.testing.expectEqualStrings(arguments_json, persisted_calls[0].arguments_json); try std.testing.expectEqualStrings(result_output, result.output); try std.testing.expectEqualStrings(preview, result.preview.?); var parsed = try std.json.parseFromSlice( std.json.Value, alloc, persisted_calls[0].arguments_json, .{}, ); defer parsed.deinit(); try std.testing.expectEqualStrings( "named-secret-123456", parsed.value.object.get("api_key").?.string, ); try std.testing.expectEqualStrings( "plain", parsed.value.object.get("nested").?.object.get("values").?.array.items[2].string, ); } test "display masking preserves benign secret-like words" { const alloc = std.testing.allocator; const input = "The tokenizer counts ordinary tokens.\n" ++ "The secretariat published a public report.\n" ++ "A token is a lexical unit.\n" ++ "Authorization is an HTTP header.\n" ++ "A cookie policy can be public."; const masked = try maskTextForDisplay(alloc, input); defer alloc.free(masked); try std.testing.expectEqualStrings(input, masked); } test "redactToolArgumentsJson redacts credential keys without hiding benign metadata" { const alloc = std.testing.allocator; const arguments_json = \\{"token_count":128,"authorization_docs":"public","cookie_policy":"strict","token":"secret-value-123456","client_secret":"hidden-value-123456"} ; const redacted = try redactToolArgumentsJson(alloc, "mcp__fixture__echo", arguments_json); defer alloc.free(redacted); var parsed = try std.json.parseFromSlice( std.json.Value, alloc, redacted, .{}, ); defer parsed.deinit(); try std.testing.expectEqual( @as(i64, 128), parsed.value.object.get("token_count").?.integer, ); try std.testing.expectEqualStrings( "public", parsed.value.object.get("authorization_docs").?.string, ); try std.testing.expectEqualStrings( "strict", parsed.value.object.get("cookie_policy").?.string, ); try std.testing.expectEqualStrings( "[REDACTED]", parsed.value.object.get("token").?.string, ); try std.testing.expectEqualStrings( "[REDACTED]", parsed.value.object.get("client_secret").?.string, ); } test "durable execution memory pseudonymizes sensitive call ids consistently" { const alloc = std.testing.allocator; const secret_id = "sk-abcdefghijklmnop"; const call = ToolCall{ .id = secret_id, .name = "read_file", .arguments_json = "{\"path\":\"notes.txt\"}", .provisional_id = secret_id, }; const result = try makePersistedToolResult( alloc, call.id, call.name, .success, "notes", .{ .model_view_covers_full_file = true }, ); defer freeTransientPersistedToolResult(alloc, result); const persisted_calls = try dupeCompletedPersistedToolCalls( alloc, &.{call}, &.{result}, ); defer types.freeToolCallSlice(alloc, persisted_calls); var files: std.ArrayList(types.FileEvidence) = .empty; defer { for (files.items) |file| freeTransientFileEvidence(alloc, file); files.deinit(alloc); } try appendFileEvidenceForTool( alloc, &files, call, .success, .{ .model_view_covers_full_file = true }, ); try std.testing.expectEqual(@as(usize, 1), persisted_calls.len); try std.testing.expectEqual(@as(usize, 1), files.items.len); try std.testing.expect(!std.mem.eql(u8, secret_id, result.tool_call_id)); try std.testing.expectEqualStrings(result.tool_call_id, persisted_calls[0].id); try std.testing.expectEqualStrings(result.tool_call_id, persisted_calls[0].provisional_id.?); try std.testing.expectEqualStrings(result.tool_call_id, files.items[0].tool_call_id); } test "durable execution memory preserves committed file presentation verbatim and reuses the call identity" { const alloc = std.testing.allocator; const secret_id = "ghp_abcdefghijklmnopqrstuvwxyz12345678901234"; const secret_text = "API_KEY=abcdefghijklmnopqrstuvwxyz"; const preview_lines = [_]types.CommittedFilePresentationLine{ .{ .kind = .addition, .new_line = 1, .text = secret_text }, .{ .kind = .elision, .text = "more secret rows omitted" }, }; const calls = [_]ToolCall{.{ .id = secret_id, .name = "write_file", .arguments_json = "{\"path\":\"notes.txt\",\"content\":\"secret\"}", }}; const messages = [_]types.ChatMessage{ .{ .role = .assistant, .tool_calls = calls[0..] }, .{ .role = .tool, .content = "wrote notes.txt", .tool_call_id = secret_id, .tool_name = "write_file", .tool_result_status = .success, .tool_result_memory = .{ .committed_file_presentation = .{ .path = "secrets/notes.txt", .kind = .added, .lines = &preview_lines, .additions = 120, .deletions = 0, .truncated = true, .after_content = secret_text ++ "\n", .lifecycle_id = .{ .turn_id = 8, .call_id = secret_id }, }, }, }, }; const memory = try buildNormalChatExecutionMemory(alloc, &messages); defer types.freeExecutionMemory(alloc, memory); const result = memory.tool_steps[0].tool_results[0]; const presentation = result.committed_file_presentation orelse return error.TestExpectedPresentation; try std.testing.expectEqualStrings("secrets/notes.txt", presentation.path); try std.testing.expectEqualStrings(secret_text, presentation.lines[0].text); try std.testing.expectEqualStrings(secret_text ++ "\n", presentation.after_content.?); try std.testing.expectEqualStrings(result.tool_call_id, presentation.lifecycle_id.?.call_id); try std.testing.expectEqualStrings(memory.tool_steps[0].tool_calls[0].id, presentation.lifecycle_id.?.call_id); try std.testing.expectEqual(@as(usize, 1), memory.files.len); try std.testing.expectEqualStrings("notes.txt", memory.files[0].path); try std.testing.expectEqual(types.FileEvidenceAction.write, memory.files[0].action); try std.testing.expectEqualStrings(result.tool_call_id, memory.files[0].tool_call_id); } test "file evidence does not parse unknown tool arguments" { var failing_allocator = std.testing.FailingAllocator.init(std.testing.allocator, .{}); const alloc = failing_allocator.allocator(); var files: std.ArrayList(types.FileEvidence) = .empty; defer files.deinit(alloc); try appendFileEvidenceForTool( alloc, &files, .{ .id = "large_command", .name = "run_command", .arguments_json = "{\"command\":\"" ++ ("x\\n" ** 20_000) ++ "\"}", }, .success, null, ); try std.testing.expectEqual(@as(usize, 0), failing_allocator.alloc_index); try std.testing.expectEqual(@as(usize, 0), files.items.len); } test "search file evidence normalizes missing and empty roots" { const alloc = std.testing.allocator; var files: std.ArrayList(types.FileEvidence) = .empty; defer { for (files.items) |file| freeTransientFileEvidence(alloc, file); files.deinit(alloc); } const calls = [_]ToolCall{ .{ .id = "call_empty_root", .name = "grep_files", .arguments_json = "{\"pattern\":\"needle\",\"path\":\"\"}", }, .{ .id = "call_default_root", .name = "glob_files", .arguments_json = "{\"pattern\":\"*.zig\"}", }, }; for (calls) |call| { try appendFileEvidenceForTool(alloc, &files, call, .success, null); } try std.testing.expectEqual(@as(usize, 2), files.items.len); try std.testing.expectEqualStrings(".", files.items[0].path); try std.testing.expectEqualStrings(".", files.items[1].path); } test "read evidence trusts typed model coverage instead of output text" { const alloc = std.testing.allocator; var files: std.ArrayList(types.FileEvidence) = .empty; defer { for (files.items) |file| freeTransientFileEvidence(alloc, file); files.deinit(alloc); } const call = ToolCall{ .id = "call_read", .name = "read_file", .arguments_json = "{\"path\":\"notes.txt\"}", }; try appendFileEvidenceForTool( alloc, &files, call, .success, null, ); try appendFileEvidenceForTool( alloc, &files, call, .success, .{ .model_view_covers_full_file = false }, ); try appendFileEvidenceForTool( alloc, &files, call, .success, .{ .model_view_covers_full_file = true }, ); try appendFileEvidenceForTool( alloc, &files, call, .success, .{ .model_view_covers_full_file = true, .truncated = true, }, ); try std.testing.expectEqual(@as(usize, 4), files.items.len); try std.testing.expect(!files.items[0].model_view_covers_full_file); try std.testing.expect(!files.items[1].model_view_covers_full_file); try std.testing.expect(files.items[2].model_view_covers_full_file); try std.testing.expect(!files.items[3].model_view_covers_full_file); } test "normal execution memory attaches marked permission feedback to its tool result" { const alloc = std.testing.allocator; var calls = [_]ToolCall{.{ .id = "call_feedback", .name = "write_file", .arguments_json = "{\"path\":\"note.txt\",\"content\":\"original\"}", }}; const messages = [_]types.ChatMessage{ .{ .role = .assistant, .tool_calls = calls[0..] }, .{ .role = .tool, .content = "wrote note.txt", .tool_call_id = "call_feedback", .tool_name = "write_file", .tool_result_status = .success, }, .{ .role = .user, .content = "read it after writing", .permission_feedback = true, }, .{ .role = .user, .content = "then summarize the exact contents", .permission_feedback = true, }, }; const memory = try buildNormalChatExecutionMemory(alloc, &messages); defer types.freeExecutionMemory(alloc, memory); try std.testing.expectEqual(@as(usize, 1), memory.tool_steps.len); const result = memory.tool_steps[0].tool_results[0]; try std.testing.expectEqual(@as(usize, 2), result.permission_feedback.len); try std.testing.expectEqualStrings("read it after writing", result.permission_feedback[0]); try std.testing.expectEqualStrings( "then summarize the exact contents", result.permission_feedback[1], ); } test "normal execution memory scans the deferred user tail by source call" { const alloc = std.testing.allocator; var calls = [_]ToolCall{ .{ .id = "call_first", .name = "run_command", .arguments_json = "{\"command\":\"printf first\"}" }, .{ .id = "call_second", .name = "run_command", .arguments_json = "{\"command\":\"printf second\"}" }, }; const messages = [_]types.ChatMessage{ .{ .role = .assistant, .tool_calls = calls[0..] }, .{ .role = .tool, .content = "first command completed", .tool_call_id = calls[0].id, .tool_name = calls[0].name, .tool_result_status = .success }, .{ .role = .tool, .content = "second command completed", .tool_call_id = calls[1].id, .tool_name = calls[1].name, .tool_result_status = .success }, .{ .role = .user, .content = "first command feedback marker", .tool_call_id = calls[0].id, .permission_feedback = true }, .{ .role = .user, .content = "custom hint", .permission_feedback = false }, .{ .role = .user, .content = "second command feedback marker", .tool_call_id = calls[1].id, .permission_feedback = true }, }; const memory = try buildNormalChatExecutionMemory(alloc, &messages); defer types.freeExecutionMemory(alloc, memory); const results = memory.tool_steps[0].tool_results; try std.testing.expectEqual(@as(usize, 1), results[0].permission_feedback.len); try std.testing.expectEqualStrings("first command feedback marker", results[0].permission_feedback[0]); try std.testing.expectEqual(@as(usize, 1), results[1].permission_feedback.len); try std.testing.expectEqualStrings("second command feedback marker", results[1].permission_feedback[0]); } test "normal execution-memory builders produce identical durable projection" { const alloc = std.testing.allocator; var first_calls = [_]ToolCall{ .{ .id = "sk-abcdefghijklmnop", .name = "read_file", .arguments_json = "{\"path\":\"notes.txt\"}", }, .{ .id = "call_search", .name = "grep_files", .arguments_json = "{\"path\":\"src\",\"query\":\"needle\"}", }, }; var second_calls = [_]ToolCall{.{ .id = "call_write", .name = "write_file", .arguments_json = "{\"path\":\"notes.txt\",\"content\":\"updated\"}", }}; const chat_messages = [_]types.ChatMessage{ .{ .role = .assistant, .content = "first assistant sk-abcdefghijklmnop", .tool_calls = first_calls[0..], }, .{ .role = .tool, .content = "ignored unmatched result", .tool_call_id = "unmatched", }, .{ .role = .tool, .content = "sk-abcdefghijklmnop full file", .tool_call_id = first_calls[0].id, .tool_name = "read_file", .tool_result_status = .success, .tool_result_memory = .{ .model_view_covers_full_file = true, }, }, .{ .role = .tool, .content = "search failed", .tool_call_id = first_calls[1].id, .tool_name = "grep_files", .tool_result_status = .failure, .tool_result_memory = .{ .output_handle = "result-call_search-grep_files.txt", .preview = "search preview", .output_bytes = 128, .stored_output_bytes = 64, .truncated = true, }, }, .{ .role = .user, .content = "after reading, summarize the file", .tool_call_id = first_calls[0].id, .permission_feedback = true, }, .{ .role = .user, .content = "separator" }, .{ .role = .assistant, .tool_calls = second_calls[0..], }, .{ .role = .tool, .tool_call_id = second_calls[0].id, .tool_name = "write_file", .tool_result_status = .success, }, }; const message_messages = [_]message.Message{ message.Message.assistantBorrowed( "first assistant sk-abcdefghijklmnop", first_calls[0..], ), .{ .role = .tool, .content = .{ .text = "ignored unmatched result" }, .tool_call_id = "unmatched", }, .{ .role = .tool, .content = .{ .text = "sk-abcdefghijklmnop full file" }, .tool_call_id = first_calls[0].id, .tool_name = "read_file", .tool_result_status = .success, .tool_result_memory = .{ .model_view_covers_full_file = true, }, }, .{ .role = .tool, .content = .{ .text = "search failed" }, .tool_call_id = first_calls[1].id, .tool_name = "grep_files", .tool_result_status = .failure, .tool_result_memory = .{ .output_handle = "result-call_search-grep_files.txt", .preview = "search preview", .output_bytes = 128, .stored_output_bytes = 64, .truncated = true, }, }, .{ .role = .user, .content = .{ .text = "after reading, summarize the file" }, .tool_call_id = first_calls[0].id, .permission_feedback = true, }, message.Message.userText("separator"), message.Message.assistantBorrowed(null, second_calls[0..]), .{ .role = .tool, .tool_call_id = second_calls[0].id, .tool_name = "write_file", .tool_result_status = .success, }, }; const chat_memory = try buildNormalChatExecutionMemory(alloc, &chat_messages); defer types.freeExecutionMemory(alloc, chat_memory); const message_memory = try buildNormalMessageExecutionMemory( alloc, &message_messages, ); defer types.freeExecutionMemory(alloc, message_memory); try expectEquivalentNormalExecutionMemory(chat_memory, message_memory); try std.testing.expectEqual(@as(usize, 2), chat_memory.tool_steps.len); try std.testing.expectEqual(@as(usize, 3), chat_memory.files.len); try std.testing.expectEqualStrings( "read_file", chat_memory.tool_steps[0].tool_calls[0].name, ); try std.testing.expectEqualStrings( "grep_files", chat_memory.tool_steps[0].tool_calls[1].name, ); try std.testing.expectEqualStrings( "read_file", chat_memory.tool_steps[0].tool_results[0].tool_name, ); try std.testing.expectEqualStrings( "grep_files", chat_memory.tool_steps[0].tool_results[1].tool_name, ); try std.testing.expectEqualStrings("notes.txt", chat_memory.files[0].path); try std.testing.expectEqualStrings("src", chat_memory.files[1].path); try std.testing.expectEqualStrings("notes.txt", chat_memory.files[2].path); try std.testing.expect(chat_memory.files[0].stale); try std.testing.expect(chat_memory.files[0].model_view_covers_full_file); try std.testing.expectEqual(types.PersistedToolStatus.failure, chat_memory.tool_steps[0].tool_results[1].status); try std.testing.expectEqual( @as(usize, 1), chat_memory.tool_steps[0].tool_results[0].permission_feedback.len, ); try std.testing.expectEqualStrings( "after reading, summarize the file", chat_memory.tool_steps[0].tool_results[0].permission_feedback[0], ); try std.testing.expectEqual( @as(usize, 0), chat_memory.tool_steps[0].tool_results[1].permission_feedback.len, ); try std.testing.expectEqualStrings("", chat_memory.tool_steps[1].tool_results[0].output); try std.testing.expectEqualStrings( chat_messages[2].content.?, chat_memory.tool_steps[0].tool_results[0].output, ); try std.testing.expect(!std.mem.eql( u8, first_calls[0].id, chat_memory.tool_steps[0].tool_calls[0].id, )); try std.testing.expectEqualStrings( chat_memory.tool_steps[0].tool_results[0].tool_call_id, chat_memory.tool_steps[0].tool_calls[0].id, ); } test "normal execution-memory builders reject missing tool result status identically" { const alloc = std.testing.allocator; var calls = [_]ToolCall{.{ .id = "call_missing_status", .name = "read_file", .arguments_json = "{\"path\":\"missing.txt\"}", }}; const chat_messages = [_]types.ChatMessage{ .{ .role = .assistant, .tool_calls = calls[0..] }, .{ .role = .tool, .content = "ignored unmatched", .tool_call_id = "unmatched", }, .{ .role = .tool, .content = "missing status", .tool_call_id = calls[0].id, .tool_name = calls[0].name, }, }; const message_messages = [_]message.Message{ message.Message.assistantBorrowed(null, calls[0..]), .{ .role = .tool, .content = .{ .text = "ignored unmatched" }, .tool_call_id = "unmatched", }, .{ .role = .tool, .content = .{ .text = "missing status" }, .tool_call_id = calls[0].id, .tool_name = calls[0].name, }, }; try std.testing.expectError( error.MissingToolResultStatus, buildNormalChatExecutionMemory(alloc, &chat_messages), ); try std.testing.expectError( error.MissingToolResultStatus, buildNormalMessageExecutionMemory(alloc, &message_messages), ); } test "normal execution-memory builders clean every allocation failure" { const ChatCheck = struct { fn run(alloc: Allocator) !void { var calls = [_]ToolCall{ .{ .id = "call_read", .name = "read_file", .arguments_json = "{\"path\":\"notes.txt\"}", }, .{ .id = "call_write", .name = "write_file", .arguments_json = "{\"path\":\"notes.txt\",\"content\":\"updated\"}", }, }; const messages = [_]types.ChatMessage{ .{ .role = .assistant, .content = "assistant", .tool_calls = calls[0..], }, .{ .role = .tool, .content = "read result", .tool_call_id = calls[0].id, .tool_name = calls[0].name, .tool_result_status = .success, .tool_result_memory = .{ .model_view_covers_full_file = true, }, }, .{ .role = .tool, .content = "write result", .tool_call_id = calls[1].id, .tool_name = calls[1].name, .tool_result_status = .success, .tool_result_memory = .{ .output_handle = "result-call_write-write_file.txt", .preview = "write preview", .output_bytes = 256, .stored_output_bytes = 128, .truncated = true, }, }, }; const memory = buildNormalChatExecutionMemory(alloc, &messages) catch |err| return switch (err) { else => err, }; defer types.freeExecutionMemory(alloc, memory); try std.testing.expectEqual(@as(usize, 2), memory.files.len); try std.testing.expect(memory.files[0].stale); } }; const MessageCheck = struct { fn run(alloc: Allocator) !void { var calls = [_]ToolCall{ .{ .id = "call_read", .name = "read_file", .arguments_json = "{\"path\":\"notes.txt\"}", }, .{ .id = "call_write", .name = "write_file", .arguments_json = "{\"path\":\"notes.txt\",\"content\":\"updated\"}", }, }; const messages = [_]message.Message{ message.Message.assistantBorrowed("assistant", calls[0..]), .{ .role = .tool, .content = .{ .text = "read result" }, .tool_call_id = calls[0].id, .tool_name = calls[0].name, .tool_result_status = .success, .tool_result_memory = .{ .model_view_covers_full_file = true, }, }, .{ .role = .tool, .content = .{ .text = "write result" }, .tool_call_id = calls[1].id, .tool_name = calls[1].name, .tool_result_status = .success, .tool_result_memory = .{ .output_handle = "result-call_write-write_file.txt", .preview = "write preview", .output_bytes = 256, .stored_output_bytes = 128, .truncated = true, }, }, }; const memory = buildNormalMessageExecutionMemory(alloc, &messages) catch |err| return switch (err) { else => err, }; defer types.freeExecutionMemory(alloc, memory); try std.testing.expectEqual(@as(usize, 2), memory.files.len); try std.testing.expect(memory.files[0].stale); } }; try std.testing.checkAllAllocationFailures( std.testing.allocator, ChatCheck.run, .{}, ); try std.testing.checkAllAllocationFailures( std.testing.allocator, MessageCheck.run, .{}, ); } test "normal execution-memory builders skip non-tool messages without allocation" { const chat_messages = [_]types.ChatMessage{ .{ .role = .system, .content = "ignored" }, .{ .role = .user, .content = "ignored" }, .{ .role = .assistant, .content = "ignored" }, }; const message_messages = [_]message.Message{ message.Message.systemText("ignored"), message.Message.userText("ignored"), message.Message.assistantBorrowed("ignored", &.{}), }; var chat_failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); var message_failing = std.testing.FailingAllocator.init( std.testing.allocator, .{ .fail_index = 0 }, ); const chat_memory = try buildNormalChatExecutionMemory( chat_failing.allocator(), &chat_messages, ); defer types.freeExecutionMemory(chat_failing.allocator(), chat_memory); const message_memory = try buildNormalMessageExecutionMemory( message_failing.allocator(), &message_messages, ); defer types.freeExecutionMemory(message_failing.allocator(), message_memory); try std.testing.expect(chat_memory.isEmpty()); try std.testing.expect(message_memory.isEmpty()); } fn expectEquivalentNormalExecutionMemory( expected: types.ExecutionMemory, actual: types.ExecutionMemory, ) !void { try std.testing.expectEqual(expected.tool_steps.len, actual.tool_steps.len); try std.testing.expectEqual(expected.files.len, actual.files.len); for (expected.tool_steps, actual.tool_steps) |expected_step, actual_step| { try expectOptionalStringEqual(expected_step.assistant, actual_step.assistant); try std.testing.expectEqual(expected_step.tool_calls.len, actual_step.tool_calls.len); try std.testing.expectEqual(expected_step.tool_results.len, actual_step.tool_results.len); for (expected_step.tool_calls, actual_step.tool_calls) |expected_call, actual_call| { try std.testing.expectEqualStrings(expected_call.id, actual_call.id); try std.testing.expectEqualStrings(expected_call.name, actual_call.name); try std.testing.expectEqualStrings( expected_call.arguments_json, actual_call.arguments_json, ); try expectOptionalStringEqual( expected_call.provisional_id, actual_call.provisional_id, ); try expectOptionalStringEqual( expected_call.provider_result, actual_call.provider_result, ); try std.testing.expectEqual( expected_call.final_identity, actual_call.final_identity, ); try std.testing.expectEqual( expected_call.provenance, actual_call.provenance, ); } for (expected_step.tool_results, actual_step.tool_results) |expected_result, actual_result| { try std.testing.expectEqualStrings( expected_result.tool_call_id, actual_result.tool_call_id, ); try std.testing.expectEqualStrings( expected_result.tool_name, actual_result.tool_name, ); try std.testing.expectEqual(expected_result.status, actual_result.status); try std.testing.expectEqualStrings(expected_result.output, actual_result.output); try expectOptionalStringEqual( expected_result.output_handle, actual_result.output_handle, ); try expectOptionalStringEqual(expected_result.preview, actual_result.preview); try std.testing.expectEqual( expected_result.output_bytes, actual_result.output_bytes, ); try std.testing.expectEqual( expected_result.stored_output_bytes, actual_result.stored_output_bytes, ); try std.testing.expectEqual(expected_result.truncated, actual_result.truncated); try std.testing.expectEqual( expected_result.provider_native, actual_result.provider_native, ); try std.testing.expectEqual( expected_result.permission_feedback.len, actual_result.permission_feedback.len, ); for (expected_result.permission_feedback, actual_result.permission_feedback) |expected_feedback, actual_feedback| { try std.testing.expectEqualStrings(expected_feedback, actual_feedback); } try std.testing.expect(expected_result.created_at_ms > 0); try std.testing.expect(actual_result.created_at_ms > 0); } } for (expected.files, actual.files) |expected_file, actual_file| { try std.testing.expectEqualStrings(expected_file.path, actual_file.path); try expectOptionalStringEqual(expected_file.new_path, actual_file.new_path); try std.testing.expectEqualStrings( expected_file.tool_call_id, actual_file.tool_call_id, ); try std.testing.expectEqualStrings(expected_file.tool_name, actual_file.tool_name); try std.testing.expectEqual(expected_file.action, actual_file.action); try std.testing.expectEqual(expected_file.status, actual_file.status); try std.testing.expectEqual( expected_file.model_view_covers_full_file, actual_file.model_view_covers_full_file, ); try std.testing.expectEqual(expected_file.stale, actual_file.stale); } } fn expectOptionalStringEqual( expected: ?[]const u8, actual: ?[]const u8, ) !void { if (expected) |expected_text| { try std.testing.expect(actual != null); try std.testing.expectEqualStrings(expected_text, actual.?); } else { try std.testing.expect(actual == null); } }