const std = @import("std");const debug_trace = @import("../shared/debug_trace.zig");const tool_result_errors = @import("../tooling/tool_result_errors.zig");const types = @import("../shared/types.zig");const session = @import("session.zig"); const Allocator = std.mem.Allocator;const legacy_schema_v1: i64 = 1;const legacy_schema_v2: i64 = 2; pub const LegacySchemaVersion = enum(u8) { v1 = 1, v2 = 2,}; const TestStoredSession = struct { id: []u8, workspace_root: ?[]u8 = null, created_at_ms: i64, updated_at_ms: i64, conversation_language: session.ConversationLanguage, history: []session.HistoryTurn, total_input_tokens: u64 = 0, total_output_tokens: u64 = 0, total_web_search_requests: u64 = 0, fn deinit(self: *TestStoredSession, alloc: Allocator) void { if (self.id.len > 0) alloc.free(self.id); if (self.workspace_root) |workspace_root| alloc.free(workspace_root); session.freeHistoryTurnSlice(alloc, self.history); self.* = undefined; }}; /// Renders one session record as JSON; caller owns the returned slice and frees with allocator.free.pub const SessionTokenUsage = struct { input: u64 = 0, output: u64 = 0, web_search_requests: u64 = 0,}; pub noinline fn renderSessionJson( alloc: Allocator, active_id: []const u8, created_at_ms: i64, updated_at_ms: i64, language: session.ConversationLanguage, workspace_root: []const u8, history: []const session.HistoryTurn, token_usage: SessionTokenUsage,) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"schema_version\":1,\"id\":"); try std.json.Stringify.value(active_id, .{}, &out.writer); try out.writer.print(",\"created_at_ms\":{d},\"updated_at_ms\":{d}", .{ created_at_ms, updated_at_ms }); try out.writer.writeAll(",\"workspace_root\":"); try std.json.Stringify.value(workspace_root, .{}, &out.writer); try out.writer.writeAll(",\"conversation_language\":"); try std.json.Stringify.value(language.view(), .{}, &out.writer); if (token_usage.input > 0 or token_usage.output > 0) { try out.writer.print(",\"total_input_tokens\":{d},\"total_output_tokens\":{d}", .{ token_usage.input, token_usage.output }); } if (token_usage.web_search_requests > 0) { try out.writer.print(",\"total_web_search_requests\":{d}", .{token_usage.web_search_requests}); } try out.writer.print(",\"history_len\":{d},\"history\":[", .{history.len}); for (history, 0..) |turn, i| { if (i > 0) try out.writer.writeByte(','); try writeHistoryTurnJson(&out.writer, turn); } try out.writer.writeAll("]}"); return try out.toOwnedSlice();} fn writeHistoryTurnJson(writer: *std.Io.Writer, turn: session.HistoryTurn) !void { switch (turn) { .compacted_summary => |entry| { try writer.writeAll("{\"kind\":\"compacted_summary\",\"summary\":"); try std.json.Stringify.value(entry.summary, .{}, writer); try writer.print(",\"removed_turn_count\":{d},\"compaction_count\":{d}", .{ entry.removed_turn_count, entry.compaction_count }); try writer.writeByte('}'); }, .assistant => |entry| { try writer.writeAll("{\"kind\":\"assistant\",\"user\":"); try writeUserTurnJson(writer, entry.user); try writer.writeAll(",\"assistant\":"); try std.json.Stringify.value(entry.assistant, .{}, writer); if (entry.execution.tool_steps.len > 0 or entry.execution.files.len > 0) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeByte('}'); }, .background_command => |entry| { try writer.writeAll("{\"kind\":\"background_command\",\"user\":"); try writeUserTurnJson(writer, entry.user); if (entry.assistant) |assistant| { try writer.writeAll(",\"assistant\":"); try std.json.Stringify.value(assistant, .{}, writer); } if (!entry.execution.isEmpty()) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeAll(",\"log_path\":"); try std.json.Stringify.value(entry.log_path, .{}, writer); try writer.print(",\"expect_url\":{s},\"url\":", .{if (entry.expect_url) "true" else "false"}); if (entry.url) |url| { try std.json.Stringify.value(url, .{}, writer); } else { try writer.writeAll("null"); } if (entry.background_record_id) |record_id| { try writer.writeAll(",\"background_record_id\":"); try writeHexString(writer, &record_id); } try writer.writeByte('}'); }, .interrupted => |entry| { try writer.writeAll("{\"kind\":\"interrupted\",\"user\":"); try writeUserTurnJson(writer, entry.user); try writer.writeAll(",\"assistant\":"); if (entry.assistant) |assistant| { try std.json.Stringify.value(assistant, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_call\":"); if (entry.tool_call) |tool_call| { try writeToolCallJson(writer, tool_call); } else { try writer.writeAll("null"); } try writer.writeAll(",\"completed_tool_names\":"); try writeStringArrayJson(writer, entry.completed_tool_names); try writer.writeAll(",\"terminal_reason\":"); try std.json.Stringify.value(@tagName(entry.terminal_reason), .{}, writer); if (!entry.execution.isEmpty()) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeByte('}'); }, }} fn writeToolCallJson(writer: *std.Io.Writer, tool_call: session.ToolCall) !void { try writer.writeAll("{\"id\":"); try std.json.Stringify.value(tool_call.id, .{}, writer); try writer.writeAll(",\"name\":"); try std.json.Stringify.value(tool_call.name, .{}, writer); try writer.writeAll(",\"arguments_json\":"); try std.json.Stringify.value(tool_call.arguments_json, .{}, writer); try writer.writeAll(",\"provider_result\":"); if (tool_call.provider_result) |provider_result| { try std.json.Stringify.value(provider_result, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeByte('}');} pub fn writeExecutionMemoryJson(writer: *std.Io.Writer, execution: session.ExecutionMemory) !void { try writer.writeAll("{\"schema_version\":2,\"tool_steps\":["); for (execution.tool_steps, 0..) |step, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"assistant\":"); if (step.assistant) |assistant| { try std.json.Stringify.value(assistant, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_calls\":["); for (step.tool_calls, 0..) |tool_call, call_index| { if (call_index > 0) try writer.writeByte(','); try writeToolCallJson(writer, tool_call); } try writer.writeAll("],\"tool_results\":["); for (step.tool_results, 0..) |result, result_index| { if (result_index > 0) try writer.writeByte(','); try writePersistedToolResultJson(writer, result); } try writer.writeAll("]}"); } try writer.writeAll("],\"files\":["); for (execution.files, 0..) |file, i| { if (i > 0) try writer.writeByte(','); try writeFileEvidenceJson(writer, file); } try writer.writeAll("]}");} fn writePersistedToolResultJson(writer: *std.Io.Writer, result: session.PersistedToolResult) !void { try writer.writeAll("{\"tool_call_id\":"); try std.json.Stringify.value(result.tool_call_id, .{}, writer); try writer.writeAll(",\"tool_name\":"); try std.json.Stringify.value(result.tool_name, .{}, writer); try writer.writeAll(",\"status\":"); try std.json.Stringify.value(@tagName(result.status), .{}, writer); try writer.writeAll(",\"output\":"); try std.json.Stringify.value(result.output, .{}, writer); if (result.output_handle) |handle| { try writer.writeAll(",\"output_handle\":"); try std.json.Stringify.value(handle, .{}, writer); } if (result.preview) |preview| { try writer.writeAll(",\"preview\":"); try std.json.Stringify.value(preview, .{}, writer); } try writer.print(",\"output_bytes\":{d},\"stored_output_bytes\":{d},\"truncated\":{s},\"provider_native\":{s},\"created_at_ms\":{d}", .{ result.output_bytes, result.stored_output_bytes, if (result.truncated) "true" else "false", if (result.provider_native) "true" else "false", result.created_at_ms, }); try writer.writeAll(",\"permission_feedback\":["); for (result.permission_feedback, 0..) |feedback, i| { if (i > 0) try writer.writeByte(','); try std.json.Stringify.value(feedback, .{}, writer); } try writer.writeByte(']'); if (result.committed_file_presentation) |presentation| { try writer.writeAll(",\"committed_file_presentation\":"); try writeCommittedFilePresentationJson(writer, presentation); } try writer.writeByte('}');} fn writeCommittedFilePresentationJson( writer: *std.Io.Writer, presentation: types.CommittedFilePresentation,) !void { try writer.writeAll("{\"path\":"); try std.json.Stringify.value(presentation.path, .{}, writer); try writer.writeAll(",\"kind\":"); try std.json.Stringify.value(@tagName(presentation.kind), .{}, writer); try writer.writeAll(",\"lines\":["); for (presentation.lines, 0..) |line, index| { if (index > 0) try writer.writeByte(','); try writer.writeAll("{\"kind\":"); try std.json.Stringify.value(@tagName(line.kind), .{}, writer); try writer.writeAll(",\"old_line\":"); try writeOptionalU32Json(writer, line.old_line); try writer.writeAll(",\"new_line\":"); try writeOptionalU32Json(writer, line.new_line); try writer.writeAll(",\"text\":"); try std.json.Stringify.value(line.text, .{}, writer); try writer.writeByte('}'); } try writer.print("],\"additions\":{d},\"deletions\":{d},\"truncated\":{s},\"previous_content\":", .{ presentation.additions, presentation.deletions, if (presentation.truncated) "true" else "false", }); try writeOptionalStringJson(writer, presentation.previous_content); try writer.writeAll(",\"after_content\":"); try writeOptionalStringJson(writer, presentation.after_content); try writer.writeAll(",\"lifecycle_id\":"); if (presentation.lifecycle_id) |id| { try writer.print("{{\"turn_id\":{d},\"call_id\":", .{id.turn_id}); try std.json.Stringify.value(id.call_id, .{}, writer); try writer.writeByte('}'); } else { try writer.writeAll("null"); } try writer.writeByte('}');} fn writeOptionalU32Json(writer: *std.Io.Writer, value: ?u32) !void { if (value) |number| { try writer.print("{d}", .{number}); } else { try writer.writeAll("null"); }} fn writeOptionalStringJson(writer: *std.Io.Writer, value: ?[]const u8) !void { if (value) |text| { try std.json.Stringify.value(text, .{}, writer); } else { try writer.writeAll("null"); }} fn writeFileEvidenceJson(writer: *std.Io.Writer, file: session.FileEvidence) !void { try writer.writeAll("{\"path\":"); try std.json.Stringify.value(file.path, .{}, writer); try writer.writeAll(",\"new_path\":"); if (file.new_path) |new_path| { try std.json.Stringify.value(new_path, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_call_id\":"); try std.json.Stringify.value(file.tool_call_id, .{}, writer); try writer.writeAll(",\"tool_name\":"); try std.json.Stringify.value(file.tool_name, .{}, writer); try writer.writeAll(",\"action\":"); try std.json.Stringify.value(@tagName(file.action), .{}, writer); try writer.writeAll(",\"status\":"); try std.json.Stringify.value(@tagName(file.status), .{}, writer); try writer.print(",\"model_view_covers_full_file\":{s},\"stale\":{s}", .{ if (file.model_view_covers_full_file) "true" else "false", if (file.stale) "true" else "false", }); try writer.writeByte('}');} fn writeUserTurnJson(writer: *std.Io.Writer, user: session.UserTurn) !void { try writer.writeAll("{\"text\":"); try std.json.Stringify.value(user.text, .{}, writer); try writer.writeAll(",\"images\":["); for (user.images, 0..) |image, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"path\":"); try std.json.Stringify.value(image.path, .{}, writer); try writer.writeAll(",\"media_type\":"); try std.json.Stringify.value(image.media_type, .{}, writer); try writer.writeAll(",\"snapshot_path\":"); try writeImageSnapshotLocatorJson(writer, image.snapshot_path); try writer.writeAll(",\"snapshot_sha256\":"); try std.json.Stringify.value(image.snapshot_sha256, .{}, writer); try writer.writeByte('}'); } try writer.writeAll("]}");} fn writeImageSnapshotLocatorJson(writer: *std.Io.Writer, value: ?[]const u8) !void { const path = value orelse { try writer.writeAll("null"); return; }; var locator_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; const locator = try session.projectSnapshotLocator(&locator_buffer, path); try std.json.Stringify.value(locator, .{}, writer);} test "ordinary session JSON presentation omits per-turn work provenance" { const alloc = std.testing.allocator; const history = [_]session.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("canonical prompt"), .work_id = @constCast("work-private"), }, .assistant = @constCast("canonical reply"), } }}; const json = try renderSessionJson( alloc, "presentation", 1, 2, session.ConversationLanguage.literal("en"), "/tmp/workspace", &history, .{}, ); defer alloc.free(json); try std.testing.expect(std.mem.find(u8, json, "work-private") == null); try std.testing.expect(std.mem.find(u8, json, "work_id") == null); try std.testing.expect(std.mem.find(u8, json, "canonical prompt") != null);} fn writeStringArrayJson(writer: *std.Io.Writer, items: anytype) !void { try writer.writeByte('['); for (items, 0..) |item, i| { if (i > 0) try writer.writeByte(','); try std.json.Stringify.value(item, .{}, writer); } try writer.writeByte(']');} /// Parses a session summary; caller owns returned fields and frees with SessionSummary.deinit.pub fn parseSessionSummary(comptime SessionSummary: type, alloc: Allocator, json_text: []const u8) !SessionSummary { var source = std.Io.Reader.fixed(json_text); return parseLegacySummaryStreaming(SessionSummary, alloc, &source) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.UnsupportedSessionSchema => return error.UnsupportedSessionSchema, else => return error.InvalidSessionFormat, };} pub fn parseLegacySummaryStreaming( comptime LegacySummary: type, alloc: Allocator, source: *std.Io.Reader,) !LegacySummary { return parseLegacySummaryStreamingImpl(LegacySummary, alloc, source) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => return error.ReadFailed, error.UnsupportedSessionSchema => return error.UnsupportedSessionSchema, else => return error.InvalidSessionFormat, };} fn parseLegacySummaryStreamingImpl( comptime LegacySummary: type, alloc: Allocator, source: *std.Io.Reader,) !LegacySummary { var reader = std.json.Reader.init(alloc, source); defer reader.deinit(); try expectToken(try reader.next(), .object_begin); var schema_version: ?i64 = null; var id: ?[]u8 = null; errdefer if (id) |owned| alloc.free(owned); var workspace_root: ?[]u8 = null; errdefer if (workspace_root) |owned| alloc.free(owned); var workspace_root_seen = false; var created_at_ms: ?i64 = null; var updated_at_ms: ?i64 = null; var conversation_language: ?session.ConversationLanguage = null; var history_len: ?usize = null; while (true) { const token = try reader.nextAllocMax(alloc, .alloc_if_needed, 64); defer freeToken(alloc, token); if (token == .object_end) break; const key = switch (token) { .string => |value| value, .allocated_string => |value| value, else => return error.InvalidSessionFormat, }; if (std.mem.eql(u8, key, "history")) { if (summaryComplete( schema_version, id, created_at_ms, updated_at_ms, conversation_language, history_len, )) { try requireLegacySchema(schema_version.?); var result: LegacySummary = .{ .id = id.?, .workspace_root = workspace_root, .created_at_ms = created_at_ms.?, .updated_at_ms = updated_at_ms.?, .conversation_language = conversation_language.?, .history_len = history_len.?, }; if (@hasField(LegacySummary, "schema_version")) { @field(result, "schema_version") = try legacySchemaVersion(schema_version.?); } return result; } try reader.skipValue(); continue; } if (std.mem.eql(u8, key, "schema_version")) { if (schema_version != null) return error.InvalidSessionFormat; schema_version = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "id")) { if (id != null) return error.InvalidSessionFormat; id = try readStringOwned(&reader, alloc, 255); } else if (std.mem.eql(u8, key, "workspace_root")) { if (workspace_root_seen) return error.InvalidSessionFormat; workspace_root_seen = true; workspace_root = try readOptionalStringOwned( &reader, alloc, std.Io.Dir.max_path_bytes, ); } else if (std.mem.eql(u8, key, "created_at_ms")) { if (created_at_ms != null) return error.InvalidSessionFormat; created_at_ms = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "updated_at_ms")) { if (updated_at_ms != null) return error.InvalidSessionFormat; updated_at_ms = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "conversation_language")) { if (conversation_language != null) return error.InvalidSessionFormat; const raw = try readStringOwned( &reader, alloc, session.ConversationLanguage.max_len, ); defer alloc.free(raw); conversation_language = try parseConversationLanguage(raw); } else if (std.mem.eql(u8, key, "history_len")) { if (history_len != null) return error.InvalidSessionFormat; const value = try readU64Token(&reader, alloc); if (value > std.math.maxInt(usize)) return error.InvalidSessionFormat; history_len = @intCast(value); } else { try reader.skipValue(); } } if (!summaryComplete( schema_version, id, created_at_ms, updated_at_ms, conversation_language,Lines 1–500 of 2232. Load more
const std = @import("std"); const debug_trace = @import("../shared/debug_trace.zig"); const tool_result_errors = @import("../tooling/tool_result_errors.zig"); const types = @import("../shared/types.zig"); const session = @import("session.zig"); const Allocator = std.mem.Allocator; const legacy_schema_v1: i64 = 1; const legacy_schema_v2: i64 = 2; pub const LegacySchemaVersion = enum(u8) { v1 = 1, v2 = 2, }; const TestStoredSession = struct { id: []u8, workspace_root: ?[]u8 = null, created_at_ms: i64, updated_at_ms: i64, conversation_language: session.ConversationLanguage, history: []session.HistoryTurn, total_input_tokens: u64 = 0, total_output_tokens: u64 = 0, total_web_search_requests: u64 = 0, fn deinit(self: *TestStoredSession, alloc: Allocator) void { if (self.id.len > 0) alloc.free(self.id); if (self.workspace_root) |workspace_root| alloc.free(workspace_root); session.freeHistoryTurnSlice(alloc, self.history); self.* = undefined; } }; /// Renders one session record as JSON; caller owns the returned slice and frees with allocator.free. pub const SessionTokenUsage = struct { input: u64 = 0, output: u64 = 0, web_search_requests: u64 = 0, }; pub noinline fn renderSessionJson( alloc: Allocator, active_id: []const u8, created_at_ms: i64, updated_at_ms: i64, language: session.ConversationLanguage, workspace_root: []const u8, history: []const session.HistoryTurn, token_usage: SessionTokenUsage, ) ![]u8 { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"schema_version\":1,\"id\":"); try std.json.Stringify.value(active_id, .{}, &out.writer); try out.writer.print(",\"created_at_ms\":{d},\"updated_at_ms\":{d}", .{ created_at_ms, updated_at_ms }); try out.writer.writeAll(",\"workspace_root\":"); try std.json.Stringify.value(workspace_root, .{}, &out.writer); try out.writer.writeAll(",\"conversation_language\":"); try std.json.Stringify.value(language.view(), .{}, &out.writer); if (token_usage.input > 0 or token_usage.output > 0) { try out.writer.print(",\"total_input_tokens\":{d},\"total_output_tokens\":{d}", .{ token_usage.input, token_usage.output }); } if (token_usage.web_search_requests > 0) { try out.writer.print(",\"total_web_search_requests\":{d}", .{token_usage.web_search_requests}); } try out.writer.print(",\"history_len\":{d},\"history\":[", .{history.len}); for (history, 0..) |turn, i| { if (i > 0) try out.writer.writeByte(','); try writeHistoryTurnJson(&out.writer, turn); } try out.writer.writeAll("]}"); return try out.toOwnedSlice(); } fn writeHistoryTurnJson(writer: *std.Io.Writer, turn: session.HistoryTurn) !void { switch (turn) { .compacted_summary => |entry| { try writer.writeAll("{\"kind\":\"compacted_summary\",\"summary\":"); try std.json.Stringify.value(entry.summary, .{}, writer); try writer.print(",\"removed_turn_count\":{d},\"compaction_count\":{d}", .{ entry.removed_turn_count, entry.compaction_count }); try writer.writeByte('}'); }, .assistant => |entry| { try writer.writeAll("{\"kind\":\"assistant\",\"user\":"); try writeUserTurnJson(writer, entry.user); try writer.writeAll(",\"assistant\":"); try std.json.Stringify.value(entry.assistant, .{}, writer); if (entry.execution.tool_steps.len > 0 or entry.execution.files.len > 0) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeByte('}'); }, .background_command => |entry| { try writer.writeAll("{\"kind\":\"background_command\",\"user\":"); try writeUserTurnJson(writer, entry.user); if (entry.assistant) |assistant| { try writer.writeAll(",\"assistant\":"); try std.json.Stringify.value(assistant, .{}, writer); } if (!entry.execution.isEmpty()) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeAll(",\"log_path\":"); try std.json.Stringify.value(entry.log_path, .{}, writer); try writer.print(",\"expect_url\":{s},\"url\":", .{if (entry.expect_url) "true" else "false"}); if (entry.url) |url| { try std.json.Stringify.value(url, .{}, writer); } else { try writer.writeAll("null"); } if (entry.background_record_id) |record_id| { try writer.writeAll(",\"background_record_id\":"); try writeHexString(writer, &record_id); } try writer.writeByte('}'); }, .interrupted => |entry| { try writer.writeAll("{\"kind\":\"interrupted\",\"user\":"); try writeUserTurnJson(writer, entry.user); try writer.writeAll(",\"assistant\":"); if (entry.assistant) |assistant| { try std.json.Stringify.value(assistant, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_call\":"); if (entry.tool_call) |tool_call| { try writeToolCallJson(writer, tool_call); } else { try writer.writeAll("null"); } try writer.writeAll(",\"completed_tool_names\":"); try writeStringArrayJson(writer, entry.completed_tool_names); try writer.writeAll(",\"terminal_reason\":"); try std.json.Stringify.value(@tagName(entry.terminal_reason), .{}, writer); if (!entry.execution.isEmpty()) { try writer.writeAll(",\"execution\":"); try writeExecutionMemoryJson(writer, entry.execution); } try writer.writeByte('}'); }, } } fn writeToolCallJson(writer: *std.Io.Writer, tool_call: session.ToolCall) !void { try writer.writeAll("{\"id\":"); try std.json.Stringify.value(tool_call.id, .{}, writer); try writer.writeAll(",\"name\":"); try std.json.Stringify.value(tool_call.name, .{}, writer); try writer.writeAll(",\"arguments_json\":"); try std.json.Stringify.value(tool_call.arguments_json, .{}, writer); try writer.writeAll(",\"provider_result\":"); if (tool_call.provider_result) |provider_result| { try std.json.Stringify.value(provider_result, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeByte('}'); } pub fn writeExecutionMemoryJson(writer: *std.Io.Writer, execution: session.ExecutionMemory) !void { try writer.writeAll("{\"schema_version\":2,\"tool_steps\":["); for (execution.tool_steps, 0..) |step, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"assistant\":"); if (step.assistant) |assistant| { try std.json.Stringify.value(assistant, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_calls\":["); for (step.tool_calls, 0..) |tool_call, call_index| { if (call_index > 0) try writer.writeByte(','); try writeToolCallJson(writer, tool_call); } try writer.writeAll("],\"tool_results\":["); for (step.tool_results, 0..) |result, result_index| { if (result_index > 0) try writer.writeByte(','); try writePersistedToolResultJson(writer, result); } try writer.writeAll("]}"); } try writer.writeAll("],\"files\":["); for (execution.files, 0..) |file, i| { if (i > 0) try writer.writeByte(','); try writeFileEvidenceJson(writer, file); } try writer.writeAll("]}"); } fn writePersistedToolResultJson(writer: *std.Io.Writer, result: session.PersistedToolResult) !void { try writer.writeAll("{\"tool_call_id\":"); try std.json.Stringify.value(result.tool_call_id, .{}, writer); try writer.writeAll(",\"tool_name\":"); try std.json.Stringify.value(result.tool_name, .{}, writer); try writer.writeAll(",\"status\":"); try std.json.Stringify.value(@tagName(result.status), .{}, writer); try writer.writeAll(",\"output\":"); try std.json.Stringify.value(result.output, .{}, writer); if (result.output_handle) |handle| { try writer.writeAll(",\"output_handle\":"); try std.json.Stringify.value(handle, .{}, writer); } if (result.preview) |preview| { try writer.writeAll(",\"preview\":"); try std.json.Stringify.value(preview, .{}, writer); } try writer.print(",\"output_bytes\":{d},\"stored_output_bytes\":{d},\"truncated\":{s},\"provider_native\":{s},\"created_at_ms\":{d}", .{ result.output_bytes, result.stored_output_bytes, if (result.truncated) "true" else "false", if (result.provider_native) "true" else "false", result.created_at_ms, }); try writer.writeAll(",\"permission_feedback\":["); for (result.permission_feedback, 0..) |feedback, i| { if (i > 0) try writer.writeByte(','); try std.json.Stringify.value(feedback, .{}, writer); } try writer.writeByte(']'); if (result.committed_file_presentation) |presentation| { try writer.writeAll(",\"committed_file_presentation\":"); try writeCommittedFilePresentationJson(writer, presentation); } try writer.writeByte('}'); } fn writeCommittedFilePresentationJson( writer: *std.Io.Writer, presentation: types.CommittedFilePresentation, ) !void { try writer.writeAll("{\"path\":"); try std.json.Stringify.value(presentation.path, .{}, writer); try writer.writeAll(",\"kind\":"); try std.json.Stringify.value(@tagName(presentation.kind), .{}, writer); try writer.writeAll(",\"lines\":["); for (presentation.lines, 0..) |line, index| { if (index > 0) try writer.writeByte(','); try writer.writeAll("{\"kind\":"); try std.json.Stringify.value(@tagName(line.kind), .{}, writer); try writer.writeAll(",\"old_line\":"); try writeOptionalU32Json(writer, line.old_line); try writer.writeAll(",\"new_line\":"); try writeOptionalU32Json(writer, line.new_line); try writer.writeAll(",\"text\":"); try std.json.Stringify.value(line.text, .{}, writer); try writer.writeByte('}'); } try writer.print("],\"additions\":{d},\"deletions\":{d},\"truncated\":{s},\"previous_content\":", .{ presentation.additions, presentation.deletions, if (presentation.truncated) "true" else "false", }); try writeOptionalStringJson(writer, presentation.previous_content); try writer.writeAll(",\"after_content\":"); try writeOptionalStringJson(writer, presentation.after_content); try writer.writeAll(",\"lifecycle_id\":"); if (presentation.lifecycle_id) |id| { try writer.print("{{\"turn_id\":{d},\"call_id\":", .{id.turn_id}); try std.json.Stringify.value(id.call_id, .{}, writer); try writer.writeByte('}'); } else { try writer.writeAll("null"); } try writer.writeByte('}'); } fn writeOptionalU32Json(writer: *std.Io.Writer, value: ?u32) !void { if (value) |number| { try writer.print("{d}", .{number}); } else { try writer.writeAll("null"); } } fn writeOptionalStringJson(writer: *std.Io.Writer, value: ?[]const u8) !void { if (value) |text| { try std.json.Stringify.value(text, .{}, writer); } else { try writer.writeAll("null"); } } fn writeFileEvidenceJson(writer: *std.Io.Writer, file: session.FileEvidence) !void { try writer.writeAll("{\"path\":"); try std.json.Stringify.value(file.path, .{}, writer); try writer.writeAll(",\"new_path\":"); if (file.new_path) |new_path| { try std.json.Stringify.value(new_path, .{}, writer); } else { try writer.writeAll("null"); } try writer.writeAll(",\"tool_call_id\":"); try std.json.Stringify.value(file.tool_call_id, .{}, writer); try writer.writeAll(",\"tool_name\":"); try std.json.Stringify.value(file.tool_name, .{}, writer); try writer.writeAll(",\"action\":"); try std.json.Stringify.value(@tagName(file.action), .{}, writer); try writer.writeAll(",\"status\":"); try std.json.Stringify.value(@tagName(file.status), .{}, writer); try writer.print(",\"model_view_covers_full_file\":{s},\"stale\":{s}", .{ if (file.model_view_covers_full_file) "true" else "false", if (file.stale) "true" else "false", }); try writer.writeByte('}'); } fn writeUserTurnJson(writer: *std.Io.Writer, user: session.UserTurn) !void { try writer.writeAll("{\"text\":"); try std.json.Stringify.value(user.text, .{}, writer); try writer.writeAll(",\"images\":["); for (user.images, 0..) |image, i| { if (i > 0) try writer.writeByte(','); try writer.writeAll("{\"path\":"); try std.json.Stringify.value(image.path, .{}, writer); try writer.writeAll(",\"media_type\":"); try std.json.Stringify.value(image.media_type, .{}, writer); try writer.writeAll(",\"snapshot_path\":"); try writeImageSnapshotLocatorJson(writer, image.snapshot_path); try writer.writeAll(",\"snapshot_sha256\":"); try std.json.Stringify.value(image.snapshot_sha256, .{}, writer); try writer.writeByte('}'); } try writer.writeAll("]}"); } fn writeImageSnapshotLocatorJson(writer: *std.Io.Writer, value: ?[]const u8) !void { const path = value orelse { try writer.writeAll("null"); return; }; var locator_buffer: [std.Io.Dir.max_path_bytes]u8 = undefined; const locator = try session.projectSnapshotLocator(&locator_buffer, path); try std.json.Stringify.value(locator, .{}, writer); } test "ordinary session JSON presentation omits per-turn work provenance" { const alloc = std.testing.allocator; const history = [_]session.HistoryTurn{.{ .assistant = .{ .user = .{ .text = @constCast("canonical prompt"), .work_id = @constCast("work-private"), }, .assistant = @constCast("canonical reply"), } }}; const json = try renderSessionJson( alloc, "presentation", 1, 2, session.ConversationLanguage.literal("en"), "/tmp/workspace", &history, .{}, ); defer alloc.free(json); try std.testing.expect(std.mem.find(u8, json, "work-private") == null); try std.testing.expect(std.mem.find(u8, json, "work_id") == null); try std.testing.expect(std.mem.find(u8, json, "canonical prompt") != null); } fn writeStringArrayJson(writer: *std.Io.Writer, items: anytype) !void { try writer.writeByte('['); for (items, 0..) |item, i| { if (i > 0) try writer.writeByte(','); try std.json.Stringify.value(item, .{}, writer); } try writer.writeByte(']'); } /// Parses a session summary; caller owns returned fields and frees with SessionSummary.deinit. pub fn parseSessionSummary(comptime SessionSummary: type, alloc: Allocator, json_text: []const u8) !SessionSummary { var source = std.Io.Reader.fixed(json_text); return parseLegacySummaryStreaming(SessionSummary, alloc, &source) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.UnsupportedSessionSchema => return error.UnsupportedSessionSchema, else => return error.InvalidSessionFormat, }; } pub fn parseLegacySummaryStreaming( comptime LegacySummary: type, alloc: Allocator, source: *std.Io.Reader, ) !LegacySummary { return parseLegacySummaryStreamingImpl(LegacySummary, alloc, source) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.ReadFailed => return error.ReadFailed, error.UnsupportedSessionSchema => return error.UnsupportedSessionSchema, else => return error.InvalidSessionFormat, }; } fn parseLegacySummaryStreamingImpl( comptime LegacySummary: type, alloc: Allocator, source: *std.Io.Reader, ) !LegacySummary { var reader = std.json.Reader.init(alloc, source); defer reader.deinit(); try expectToken(try reader.next(), .object_begin); var schema_version: ?i64 = null; var id: ?[]u8 = null; errdefer if (id) |owned| alloc.free(owned); var workspace_root: ?[]u8 = null; errdefer if (workspace_root) |owned| alloc.free(owned); var workspace_root_seen = false; var created_at_ms: ?i64 = null; var updated_at_ms: ?i64 = null; var conversation_language: ?session.ConversationLanguage = null; var history_len: ?usize = null; while (true) { const token = try reader.nextAllocMax(alloc, .alloc_if_needed, 64); defer freeToken(alloc, token); if (token == .object_end) break; const key = switch (token) { .string => |value| value, .allocated_string => |value| value, else => return error.InvalidSessionFormat, }; if (std.mem.eql(u8, key, "history")) { if (summaryComplete( schema_version, id, created_at_ms, updated_at_ms, conversation_language, history_len, )) { try requireLegacySchema(schema_version.?); var result: LegacySummary = .{ .id = id.?, .workspace_root = workspace_root, .created_at_ms = created_at_ms.?, .updated_at_ms = updated_at_ms.?, .conversation_language = conversation_language.?, .history_len = history_len.?, }; if (@hasField(LegacySummary, "schema_version")) { @field(result, "schema_version") = try legacySchemaVersion(schema_version.?); } return result; } try reader.skipValue(); continue; } if (std.mem.eql(u8, key, "schema_version")) { if (schema_version != null) return error.InvalidSessionFormat; schema_version = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "id")) { if (id != null) return error.InvalidSessionFormat; id = try readStringOwned(&reader, alloc, 255); } else if (std.mem.eql(u8, key, "workspace_root")) { if (workspace_root_seen) return error.InvalidSessionFormat; workspace_root_seen = true; workspace_root = try readOptionalStringOwned( &reader, alloc, std.Io.Dir.max_path_bytes, ); } else if (std.mem.eql(u8, key, "created_at_ms")) { if (created_at_ms != null) return error.InvalidSessionFormat; created_at_ms = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "updated_at_ms")) { if (updated_at_ms != null) return error.InvalidSessionFormat; updated_at_ms = try readI64Token(&reader, alloc); } else if (std.mem.eql(u8, key, "conversation_language")) { if (conversation_language != null) return error.InvalidSessionFormat; const raw = try readStringOwned( &reader, alloc, session.ConversationLanguage.max_len, ); defer alloc.free(raw); conversation_language = try parseConversationLanguage(raw); } else if (std.mem.eql(u8, key, "history_len")) { if (history_len != null) return error.InvalidSessionFormat; const value = try readU64Token(&reader, alloc); if (value > std.math.maxInt(usize)) return error.InvalidSessionFormat; history_len = @intCast(value); } else { try reader.skipValue(); } } if (!summaryComplete( schema_version, id, created_at_ms, updated_at_ms, conversation_language,