Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755const 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); }}