const std = @import("std");const lifecycle_hooks = @import("../../hooks/hooks.zig");const types = @import("../../shared/types.zig");const debug_trace = @import("../../shared/debug_trace.zig");const execution_memory_helpers = @import("../execution_memory.zig"); const runtime_execution_memory = @import("execution_memory.zig");const runtime_finalization = @import("finalization.zig");const runtime_deps = @import("deps.zig");const runtime_telemetry = @import("telemetry.zig");const worker_runtime = @import("../worker_runtime.zig"); const Allocator = std.mem.Allocator;const HistoryTurn = types.HistoryTurn;const ToolCall = types.ToolCall;const TraceContext = debug_trace.TraceContext;const AgentRuntimeDeps = runtime_deps.AgentRuntimeDeps;const TurnFinalizationGuard = runtime_finalization.TurnFinalizationGuard;const QueuedPrompt = worker_runtime.QueuedPrompt; pub fn persistInterruptedTurnOnce( hooks: *const AgentRuntimeDeps, finalization: *TurnFinalizationGuard, job: QueuedPrompt, partial_assistant: ?[]const u8, active_tool_call: ?ToolCall, completed_tool_names: [][]u8, persisted: *bool, trace_ctx: TraceContext, current_turn_messages: []const types.ChatMessage, retained_candidate: ?[]const u8, terminal_materializing: *bool,) !void { if (persisted.*) return; const durable_active_tool_call = if (active_tool_call) |call| try execution_memory_helpers.dupeRedactedToolCall( std.heap.c_allocator, call, ) else null; defer if (durable_active_tool_call) |call| { types.freeToolCall(std.heap.c_allocator, call); }; const execution = try runtime_execution_memory.buildInterruptedExecutionMemory( std.heap.c_allocator, current_turn_messages, active_tool_call, ); defer types.freeExecutionMemory(std.heap.c_allocator, execution); terminal_materializing.* = true; if (retained_candidate) |candidate| { const assistant = try lifecycle_hooks.prompt.joinVisibleSegments( std.heap.c_allocator, candidate, partial_assistant, ); defer std.heap.c_allocator.free(@constCast(assistant)); const turn: HistoryTurn = .{ .interrupted = .{ .user = .{ .text = job.prompt, .images = job.images }, .assistant = @constCast(assistant), .tool_call = durable_active_tool_call, .completed_tool_names = completed_tool_names, .execution = execution, } }; const finished = try types.dupeFinishedPrompt( std.heap.c_allocator, .{ .turn = turn, .terminal_projection = .assistant_text, }, ); persisted.* = true; var propagation_error: ?anyerror = null; hooks.propagate_history_turn(hooks.ctx, turn) catch |err| { propagation_error = err; }; try traceInterruptedPersistence( job, assistant, active_tool_call, completed_tool_names, trace_ctx, ); try finalization.finish(.interrupted, null, finished); debug_trace.eventf( "interrupt", "finish_event_emitted", trace_ctx, "outcome_kind=interrupted", .{}, ); if (propagation_error) |err| return err; return; } persisted.* = true; const turn: HistoryTurn = .{ .interrupted = .{ .user = .{ .text = job.prompt, .images = job.images }, .assistant = if (partial_assistant) |text| if (text.len > 0) @constCast(text) else null else null, .tool_call = durable_active_tool_call, .completed_tool_names = completed_tool_names, .execution = execution, } }; var propagation_error: ?anyerror = null; hooks.propagate_history_turn(hooks.ctx, turn) catch |err| { propagation_error = err; }; try traceInterruptedPersistence( job, partial_assistant, active_tool_call, completed_tool_names, trace_ctx, ); try finalization.finish(.interrupted, null, .{ .turn = try types.dupeHistoryTurn(std.heap.c_allocator, turn), }); debug_trace.eventf("interrupt", "finish_event_emitted", trace_ctx, "outcome_kind=interrupted", .{}); if (propagation_error) |err| return err;} pub fn persistFailedPartialTurnOnce( hooks: *const AgentRuntimeDeps, finalization: *TurnFinalizationGuard, job: QueuedPrompt, partial_assistant: []const u8, persisted: *bool, trace_ctx: TraceContext, current_turn_messages: []const types.ChatMessage, terminal_materializing: *bool,) !void { if (persisted.*) return; if (partial_assistant.len == 0) return; const execution = try runtime_execution_memory.buildInterruptedExecutionMemory( std.heap.c_allocator, current_turn_messages, null, ); defer types.freeExecutionMemory(std.heap.c_allocator, execution); terminal_materializing.* = true; const turn: HistoryTurn = .{ .interrupted = .{ .user = .{ .text = job.prompt, .images = job.images }, .assistant = @constCast(partial_assistant), .execution = execution, .terminal_reason = .failed, } }; const finished = try types.dupeFinishedPrompt( std.heap.c_allocator, .{ .turn = turn, .terminal_projection = .assistant_text, }, ); persisted.* = true; var propagation_error: ?anyerror = null; hooks.propagate_history_turn(hooks.ctx, turn) catch |err| { propagation_error = err; }; debug_trace.eventf( "gateway", "stream_failure_history_persisted", trace_ctx, "prompt_bytes={d} partial_assistant_bytes={d}", .{ job.prompt.len, partial_assistant.len }, ); try finalization.finish(.failed, null, finished); debug_trace.eventf( "gateway", "stream_failure_finish_event_emitted", trace_ctx, "outcome_kind=failed", .{}, ); if (propagation_error) |err| return err;} fn traceInterruptedPersistence( job: QueuedPrompt, partial_assistant: ?[]const u8, active_tool_call: ?ToolCall, completed_tool_names: [][]u8, trace_ctx: TraceContext,) !void { const completed_tool_names_text = try runtime_telemetry.formatCompletedToolNamesCompact(std.heap.c_allocator, completed_tool_names); defer std.heap.c_allocator.free(completed_tool_names_text); debug_trace.logf("agent", "interrupted marker persisted prompt_bytes={d} partial_assistant_bytes={d} active_tool={s} completed_tool_count={d} completed_tool_names={s}", .{ job.prompt.len, if (partial_assistant) |text| text.len else 0, if (active_tool_call != null) "true" else "false", completed_tool_names.len, completed_tool_names_text, }); debug_trace.eventf("interrupt", "interrupted_history_persisted", trace_ctx, "prompt_bytes={d} partial_assistant_bytes={d} active_tool_known={s} completed_tool_count={d} completed_tool_names={s}", .{ job.prompt.len, if (partial_assistant) |text| text.len else 0, if (active_tool_call != null) "true" else "false", completed_tool_names.len, completed_tool_names_text, }); debug_trace.eventf("interrupt", "interrupt_persisted", trace_ctx, "prompt_bytes={d} partial_assistant_bytes={d} active_tool={s} completed_tool_count={d} completed_tool_names={s} active_tool_reason={s}", .{ job.prompt.len, if (partial_assistant) |text| text.len else 0, if (active_tool_call != null) "true" else "false", completed_tool_names.len, completed_tool_names_text, interruptPersistenceReason(partial_assistant, active_tool_call, completed_tool_names), }); if (active_tool_call) |tool_call| { debug_trace.logf("agent", "aborted tool output persisted call_id={s} name={s}", .{ tool_call.id, tool_call.name }); debug_trace.eventf("interrupt", "aborted_tool_persisted", trace_ctx, "call_id={s} name={s}", .{ tool_call.id, tool_call.name }); }} pub fn recordCompletedToolName(arena: Allocator, completed_tool_names: *std.ArrayList([]u8), name: []const u8) !void { try completed_tool_names.append(arena, try arena.dupe(u8, name));} pub fn countInterruptedHistory(history: []const HistoryTurn) usize { var count: usize = 0; for (history) |turn| { if (turn == .interrupted) count += 1; } return count;} pub fn countPartialTextInterruptedClosures(history: []const HistoryTurn) usize { var count: usize = 0; for (history) |turn| { const entry = switch (turn) { .interrupted => |value| value, else => continue, }; if (entry.tool_call != null) continue; if (entry.completed_tool_names.len > 0) continue; if (entry.assistant) |assistant| { if (assistant.len > 0) count += 1; } } return count;} fn interruptPersistenceReason(partial_assistant: ?[]const u8, active_tool_call: ?ToolCall, completed_tool_names: []const []u8) []const u8 { if (active_tool_call != null) return "active_tool_call_present"; if (completed_tool_names.len > 0) return "completed_tools_present"; if (partial_assistant) |text| { if (text.len > 0) return "partial_assistant_only"; } return "no_assistant_output";}
const std = @import("std");
const lifecycle_hooks = @import("../../hooks/hooks.zig");
const types = @import("../../shared/types.zig");
const debug_trace = @import("../../shared/debug_trace.zig");
const execution_memory_helpers = @import("../execution_memory.zig");
const runtime_execution_memory = @import("execution_memory.zig");
const runtime_finalization = @import("finalization.zig");
const runtime_deps = @import("deps.zig");
const runtime_telemetry = @import("telemetry.zig");
const worker_runtime = @import("../worker_runtime.zig");
const Allocator = std.mem.Allocator;
const HistoryTurn = types.HistoryTurn;
const ToolCall = types.ToolCall;
const TraceContext = debug_trace.TraceContext;
const AgentRuntimeDeps = runtime_deps.AgentRuntimeDeps;
const TurnFinalizationGuard = runtime_finalization.TurnFinalizationGuard;
const QueuedPrompt = worker_runtime.QueuedPrompt;
pub fn persistInterruptedTurnOnce(
hooks: *const AgentRuntimeDeps,
finalization: *TurnFinalizationGuard,
job: QueuedPrompt,
partial_assistant: ?[]const u8,
active_tool_call: ?ToolCall,
completed_tool_names: [][]u8,
persisted: *bool,
trace_ctx: TraceContext,
current_turn_messages: []const types.ChatMessage,
retained_candidate: ?[]const u8,
terminal_materializing: *bool,
) !void {
if (persisted.*) return;
const durable_active_tool_call = if (active_tool_call) |call|
try execution_memory_helpers.dupeRedactedToolCall(
std.heap.c_allocator,
call,
)
else
null;
defer if (durable_active_tool_call) |call| {
types.freeToolCall(std.heap.c_allocator, call);
};
const execution = try runtime_execution_memory.buildInterruptedExecutionMemory(
std.heap.c_allocator,
current_turn_messages,
active_tool_call,
);
defer types.freeExecutionMemory(std.heap.c_allocator, execution);
terminal_materializing.* = true;
if (retained_candidate) |candidate| {
const assistant = try lifecycle_hooks.prompt.joinVisibleSegments(
std.heap.c_allocator,
candidate,
partial_assistant,
);
defer std.heap.c_allocator.free(@constCast(assistant));
const turn: HistoryTurn = .{ .interrupted = .{
.user = .{ .text = job.prompt, .images = job.images },
.assistant = @constCast(assistant),
.tool_call = durable_active_tool_call,
.completed_tool_names = completed_tool_names,
.execution = execution,
} };
const finished = try types.dupeFinishedPrompt(
std.heap.c_allocator,
.{
.turn = turn,
.terminal_projection = .assistant_text,
},
);
persisted.* = true;
var propagation_error: ?anyerror = null;
hooks.propagate_history_turn(hooks.ctx, turn) catch |err| {
propagation_error = err;
};
try traceInterruptedPersistence(
job,
assistant,
active_tool_call,
completed_tool_names,
trace_ctx,
);
try finalization.finish(.interrupted, null, finished);
debug_trace.eventf(
"interrupt",
"finish_event_emitted",
trace_ctx,
"outcome_kind=interrupted",
.{},
);
if (propagation_error) |err| return err;
return;
}
persisted.* = true;
const turn: HistoryTurn = .{ .interrupted = .{
.user = .{ .text = job.prompt, .images = job.images },
.assistant = if (partial_assistant) |text| if (text.len > 0) @constCast(text) else null else null,
.tool_call = durable_active_tool_call,
.completed_tool_names = completed_tool_names,
.execution = execution,
} };
var propagation_error: ?anyerror = null;
hooks.propagate_history_turn(hooks.ctx, turn) catch |err| {
propagation_error = err;
};
try traceInterruptedPersistence(
job,
partial_assistant,
active_tool_call,
completed_tool_names,
trace_ctx,
);
try finalization.finish(.interrupted, null, .{
.turn = try types.dupeHistoryTurn(std.heap.c_allocator, turn),
});
debug_trace.eventf("interrupt", "finish_event_emitted", trace_ctx, "outcome_kind=interrupted", .{});
if (propagation_error) |err| return err;
}
pub fn persistFailedPartialTurnOnce(
hooks: *const AgentRuntimeDeps,
finalization: *TurnFinalizationGuard,
job: QueuedPrompt,
partial_assistant: []const u8,
persisted: *bool,
trace_ctx: TraceContext,
current_turn_messages: []const types.ChatMessage,
terminal_materializing: *bool,
) !void {
if (persisted.*) return;
if (partial_assistant.len == 0) return;
const execution = try runtime_execution_memory.buildInterruptedExecutionMemory(
std.heap.c_allocator,
current_turn_messages,
null,
);
defer types.freeExecutionMemory(std.heap.c_allocator, execution);
terminal_materializing.* = true;
const turn: HistoryTurn = .{ .interrupted = .{
.user = .{ .text = job.prompt, .images = job.images },
.assistant = @constCast(partial_assistant),
.execution = execution,
.terminal_reason = .failed,
} };
const finished = try types.dupeFinishedPrompt(
std.heap.c_allocator,
.{
.turn = turn,
.terminal_projection = .assistant_text,
},
);
persisted.* = true;
var propagation_error: ?anyerror = null;
hooks.propagate_history_turn(hooks.ctx, turn) catch |err| {
propagation_error = err;
};
debug_trace.eventf(
"gateway",
"stream_failure_history_persisted",
trace_ctx,
"prompt_bytes={d} partial_assistant_bytes={d}",
.{ job.prompt.len, partial_assistant.len },
);
try finalization.finish(.failed, null, finished);
debug_trace.eventf(
"gateway",
"stream_failure_finish_event_emitted",
trace_ctx,
"outcome_kind=failed",
.{},
);
if (propagation_error) |err| return err;
}
fn traceInterruptedPersistence(
job: QueuedPrompt,
partial_assistant: ?[]const u8,
active_tool_call: ?ToolCall,
completed_tool_names: [][]u8,
trace_ctx: TraceContext,
) !void {
const completed_tool_names_text = try runtime_telemetry.formatCompletedToolNamesCompact(std.heap.c_allocator, completed_tool_names);
defer std.heap.c_allocator.free(completed_tool_names_text);
debug_trace.logf("agent", "interrupted marker persisted prompt_bytes={d} partial_assistant_bytes={d} active_tool={s} completed_tool_count={d} completed_tool_names={s}", .{
job.prompt.len,
if (partial_assistant) |text| text.len else 0,
if (active_tool_call != null) "true" else "false",
completed_tool_names.len,
completed_tool_names_text,
});
debug_trace.eventf("interrupt", "interrupted_history_persisted", trace_ctx, "prompt_bytes={d} partial_assistant_bytes={d} active_tool_known={s} completed_tool_count={d} completed_tool_names={s}", .{
job.prompt.len,
if (partial_assistant) |text| text.len else 0,
if (active_tool_call != null) "true" else "false",
completed_tool_names.len,
completed_tool_names_text,
});
debug_trace.eventf("interrupt", "interrupt_persisted", trace_ctx, "prompt_bytes={d} partial_assistant_bytes={d} active_tool={s} completed_tool_count={d} completed_tool_names={s} active_tool_reason={s}", .{
job.prompt.len,
if (partial_assistant) |text| text.len else 0,
if (active_tool_call != null) "true" else "false",
completed_tool_names.len,
completed_tool_names_text,
interruptPersistenceReason(partial_assistant, active_tool_call, completed_tool_names),
});
if (active_tool_call) |tool_call| {
debug_trace.logf("agent", "aborted tool output persisted call_id={s} name={s}", .{ tool_call.id, tool_call.name });
debug_trace.eventf("interrupt", "aborted_tool_persisted", trace_ctx, "call_id={s} name={s}", .{ tool_call.id, tool_call.name });
}
}
pub fn recordCompletedToolName(arena: Allocator, completed_tool_names: *std.ArrayList([]u8), name: []const u8) !void {
try completed_tool_names.append(arena, try arena.dupe(u8, name));
}
pub fn countInterruptedHistory(history: []const HistoryTurn) usize {
var count: usize = 0;
for (history) |turn| {
if (turn == .interrupted) count += 1;
}
return count;
}
pub fn countPartialTextInterruptedClosures(history: []const HistoryTurn) usize {
var count: usize = 0;
for (history) |turn| {
const entry = switch (turn) {
.interrupted => |value| value,
else => continue,
};
if (entry.tool_call != null) continue;
if (entry.completed_tool_names.len > 0) continue;
if (entry.assistant) |assistant| {
if (assistant.len > 0) count += 1;
}
}
return count;
}
fn interruptPersistenceReason(partial_assistant: ?[]const u8, active_tool_call: ?ToolCall, completed_tool_names: []const []u8) []const u8 {
if (active_tool_call != null) return "active_tool_call_present";
if (completed_tool_names.len > 0) return "completed_tools_present";
if (partial_assistant) |text| {
if (text.len > 0) return "partial_assistant_only";
}
return "no_assistant_output";
}