const std = @import("std");const model_capabilities = @import("../config/model_capabilities.zig");const image_attachments = @import("../images/image_attachments.zig");const debug_trace = @import("../shared/debug_trace.zig");const types = @import("../shared/types.zig");const tool_dispatch = @import("../tooling/tool_dispatch.zig"); const Allocator = std.mem.Allocator; pub const StreamCallback = *const fn (ctx: *anyopaque, chunk: []const u8) void;pub const ToolStartCallback = *const fn ( ctx: *anyopaque, tool_id: []const u8, tool_name: []const u8, label_value: ?[]const u8,) void; /// Monotonic evidence for whether a request could have reached its provider./// Core uses it to distinguish safe retries from potentially billed delivery.pub const DeliveryCertainty = struct { state: std.atomic.Value(State) = .init(.definitely_unsent), pub const State = enum(u8) { definitely_unsent, possibly_sent, }; pub fn init() DeliveryCertainty { return .{}; } pub fn markPossiblySent(self: *DeliveryCertainty) void { self.state.store(.possibly_sent, .seq_cst); } pub fn load(self: *const DeliveryCertainty) State { return self.state.load(.seq_cst); }}; /// Selects the layer that owns retries for a provider request./// Agent-owned model attempts must not be retried again by the transport.pub const ProviderAttemptOwner = enum { transport, agent,}; pub const NetworkFailureCause = enum { transport_interrupted, system_resumed,}; /// Stable native transport evidence consumed by model recovery policy./// Providers that cannot distinguish failure stages leave this unset.pub const NetworkFailureEvidence = struct { cause: NetworkFailureCause, delivery: DeliveryCertainty.State,}; pub const AttemptEvidence = struct { provider_admitted: bool = false, network_failure: ?NetworkFailureEvidence = null,}; /// Gives a cooperative single-threaded host a chance to publish UI and runtime/// state while provider transport remains pending.pub const CooperativePulse = struct { ctx: *anyopaque, run: *const fn (ctx: *anyopaque) anyerror!void, pub fn pulse(self: CooperativePulse) anyerror!void { try self.run(self.ctx); }}; pub const VisionMode = enum { unavailable, optional, required,}; pub const BuildBudget = struct { deadline: ?std.Io.Clock.Timestamp = null, cancel_flag: ?*std.atomic.Value(bool) = null,}; pub const StructuredResponseFormat = struct { name: []const u8, description: []const u8, schema_json: []const u8,}; pub const BuildRequest = struct { model: []const u8, tool_registry: tool_dispatch.Registry = .{}, serialized_tools: []const u8, messages: []const types.ChatMessage, tool_choice: types.ToolChoice, selected_dynamic_tool_schemas: []const []const u8 = &.{}, vision_mode: VisionMode = .unavailable, provider_options: model_capabilities.ResolvedProviderOptions, max_output_tokens: ?u32 = null, budget: ?BuildBudget = null, verified_images: ?[]const image_attachments.VerifiedSnapshot = null, response_format: ?StructuredResponseFormat = null,}; pub const Request = struct { api_key: []const u8, team: ?[]const u8, /// Borrowed for the duration of `Provider.stream`. session_id: ?[]const u8 = null, model: []const u8, retry_count: usize, chat_url: []const u8, payload: []const u8, trace_ctx: debug_trace.TraceContext, content_capture_limit: ?usize, cooperative_pulse: ?CooperativePulse = null, delivery: *DeliveryCertainty, attempt_evidence: *AttemptEvidence, callback_ctx: *anyopaque, on_content_chunk: StreamCallback, on_tool_start: ?ToolStartCallback, on_reasoning_chunk: ?StreamCallback, on_tool_input_chunk: ?StreamCallback = null, cancel_flag: *std.atomic.Value(bool), provider_attempt_owner: ProviderAttemptOwner = .transport,}; pub const ResultOwnership = enum { borrowed, owned,}; pub const Result = struct { status: std.http.Status, completion: types.GatewayCompletion = .{}, err_body: ?[]u8 = null, /// Borrowed base URL used to reconcile generation usage for this result. generation_origin: []const u8 = "", reconcile_generation_usage: bool = false, failure_schema: ?[]u8 = null, failure_request_shape: ?[]u8 = null, retry_after_seconds: ?u64 = null, ownership: ResultOwnership = .borrowed, /// Providers mark allocated response fields as `owned`; test and embedded /// providers may return stable borrowed fields instead. pub fn deinit(self: *Result, alloc: Allocator) void { if (self.ownership == .owned) { if (self.err_body) |body| alloc.free(body); if (self.completion.content) |content| alloc.free(@constCast(content)); if (self.completion.generation_id) |id| alloc.free(@constCast(id)); if (self.completion.billing) |billing| alloc.free(@constCast(billing.model)); types.freeToolCallSlice(alloc, @constCast(self.completion.tool_calls)); if (self.completion.provider_failure_detail) |detail| alloc.free(@constCast(detail)); if (self.failure_schema) |schema| alloc.free(schema); if (self.failure_request_shape) |shape| alloc.free(shape); } const status = self.status; self.* = .{ .status = status }; }}; pub const StreamFn = *const fn ( context: ?*anyopaque, alloc: Allocator, request: Request,) anyerror!Result; pub const BuildFn = *const fn ( context: ?*anyopaque, alloc: Allocator, request: BuildRequest,) anyerror![]u8; pub const Provider = struct { /// When set, context must remain valid until every in-flight `stream` returns. context: ?*anyopaque = null, build_fn: BuildFn = unavailableBuild, stream_fn: StreamFn, /// The returned request bytes are owned by `alloc`. pub fn build(self: Provider, alloc: Allocator, request: BuildRequest) ![]u8 { return self.build_fn(self.context, alloc, request); } pub fn stream(self: Provider, alloc: Allocator, request: Request) !Result { return self.stream_fn(self.context, alloc, request); }}; fn unavailableBuild(_: ?*anyopaque, _: Allocator, _: BuildRequest) anyerror![]u8 { return error.AgentStreamProviderUnavailable;} fn unavailableStream(_: ?*anyopaque, _: Allocator, _: Request) anyerror!Result { return error.AgentStreamProviderUnavailable;} pub const unavailable_provider = Provider{ .stream_fn = unavailableStream,}; test "stream provider dispatches request and preserves ordered callbacks" { const Fake = struct { calls: usize = 0, attempt_owner: ?ProviderAttemptOwner = null, fn stream(raw: ?*anyopaque, _: Allocator, request: Request) anyerror!Result { const self: *@This() = @ptrCast(@alignCast(raw.?)); self.calls += 1; self.attempt_owner = request.provider_attempt_owner; request.on_content_chunk(request.callback_ctx, "first"); request.on_reasoning_chunk.?(request.callback_ctx, "second"); return .{ .status = .ok, .completion = .{ .content = "done" }, .retry_after_seconds = 17, }; } }; const Capture = struct { chunks: std.ArrayList(u8) = .empty, failed: bool = false, fn append(raw: *anyopaque, chunk: []const u8) void { const self: *@This() = @ptrCast(@alignCast(raw)); self.chunks.appendSlice(std.testing.allocator, chunk) catch { self.failed = true; }; } }; var fake: Fake = .{}; var capture: Capture = .{}; defer capture.chunks.deinit(std.testing.allocator); var delivery = DeliveryCertainty.init(); var attempt_evidence: AttemptEvidence = .{}; var cancelled = std.atomic.Value(bool).init(false); var result = try (Provider{ .context = &fake, .stream_fn = Fake.stream, }).stream(std.testing.allocator, .{ .api_key = "key", .team = null, .model = "model", .retry_count = 1, .chat_url = "https://example.test/chat", .payload = "{}", .trace_ctx = .{}, .content_capture_limit = null, .delivery = &delivery, .attempt_evidence = &attempt_evidence, .callback_ctx = &capture, .on_content_chunk = Capture.append, .on_tool_start = null, .on_reasoning_chunk = Capture.append, .cancel_flag = &cancelled, .provider_attempt_owner = .agent, }); defer result.deinit(std.testing.allocator); try std.testing.expectEqual(@as(usize, 1), fake.calls); try std.testing.expectEqual(ProviderAttemptOwner.agent, fake.attempt_owner.?); try std.testing.expect(!capture.failed); try std.testing.expectEqualStrings("firstsecond", capture.chunks.items); try std.testing.expectEqualStrings("done", result.completion.content.?); try std.testing.expectEqual(@as(?u64, 17), result.retry_after_seconds);}
const std = @import("std");
const model_capabilities = @import("../config/model_capabilities.zig");
const image_attachments = @import("../images/image_attachments.zig");
const debug_trace = @import("../shared/debug_trace.zig");
const types = @import("../shared/types.zig");
const tool_dispatch = @import("../tooling/tool_dispatch.zig");
const Allocator = std.mem.Allocator;
pub const StreamCallback = *const fn (ctx: *anyopaque, chunk: []const u8) void;
pub const ToolStartCallback = *const fn (
ctx: *anyopaque,
tool_id: []const u8,
tool_name: []const u8,
label_value: ?[]const u8,
) void;
/// Monotonic evidence for whether a request could have reached its provider.
/// Core uses it to distinguish safe retries from potentially billed delivery.
pub const DeliveryCertainty = struct {
state: std.atomic.Value(State) = .init(.definitely_unsent),
pub const State = enum(u8) {
definitely_unsent,
possibly_sent,
};
pub fn init() DeliveryCertainty {
return .{};
}
pub fn markPossiblySent(self: *DeliveryCertainty) void {
self.state.store(.possibly_sent, .seq_cst);
}
pub fn load(self: *const DeliveryCertainty) State {
return self.state.load(.seq_cst);
}
};
/// Selects the layer that owns retries for a provider request.
/// Agent-owned model attempts must not be retried again by the transport.
pub const ProviderAttemptOwner = enum {
transport,
agent,
};
pub const NetworkFailureCause = enum {
transport_interrupted,
system_resumed,
};
/// Stable native transport evidence consumed by model recovery policy.
/// Providers that cannot distinguish failure stages leave this unset.
pub const NetworkFailureEvidence = struct {
cause: NetworkFailureCause,
delivery: DeliveryCertainty.State,
};
pub const AttemptEvidence = struct {
provider_admitted: bool = false,
network_failure: ?NetworkFailureEvidence = null,
};
/// Gives a cooperative single-threaded host a chance to publish UI and runtime
/// state while provider transport remains pending.
pub const CooperativePulse = struct {
ctx: *anyopaque,
run: *const fn (ctx: *anyopaque) anyerror!void,
pub fn pulse(self: CooperativePulse) anyerror!void {
try self.run(self.ctx);
}
};
pub const VisionMode = enum {
unavailable,
optional,
required,
};
pub const BuildBudget = struct {
deadline: ?std.Io.Clock.Timestamp = null,
cancel_flag: ?*std.atomic.Value(bool) = null,
};
pub const StructuredResponseFormat = struct {
name: []const u8,
description: []const u8,
schema_json: []const u8,
};
pub const BuildRequest = struct {
model: []const u8,
tool_registry: tool_dispatch.Registry = .{},
serialized_tools: []const u8,
messages: []const types.ChatMessage,
tool_choice: types.ToolChoice,
selected_dynamic_tool_schemas: []const []const u8 = &.{},
vision_mode: VisionMode = .unavailable,
provider_options: model_capabilities.ResolvedProviderOptions,
max_output_tokens: ?u32 = null,
budget: ?BuildBudget = null,
verified_images: ?[]const image_attachments.VerifiedSnapshot = null,
response_format: ?StructuredResponseFormat = null,
};
pub const Request = struct {
api_key: []const u8,
team: ?[]const u8,
/// Borrowed for the duration of `Provider.stream`.
session_id: ?[]const u8 = null,
model: []const u8,
retry_count: usize,
chat_url: []const u8,
payload: []const u8,
trace_ctx: debug_trace.TraceContext,
content_capture_limit: ?usize,
cooperative_pulse: ?CooperativePulse = null,
delivery: *DeliveryCertainty,
attempt_evidence: *AttemptEvidence,
callback_ctx: *anyopaque,
on_content_chunk: StreamCallback,
on_tool_start: ?ToolStartCallback,
on_reasoning_chunk: ?StreamCallback,
on_tool_input_chunk: ?StreamCallback = null,
cancel_flag: *std.atomic.Value(bool),
provider_attempt_owner: ProviderAttemptOwner = .transport,
};
pub const ResultOwnership = enum {
borrowed,
owned,
};
pub const Result = struct {
status: std.http.Status,
completion: types.GatewayCompletion = .{},
err_body: ?[]u8 = null,
/// Borrowed base URL used to reconcile generation usage for this result.
generation_origin: []const u8 = "",
reconcile_generation_usage: bool = false,
failure_schema: ?[]u8 = null,
failure_request_shape: ?[]u8 = null,
retry_after_seconds: ?u64 = null,
ownership: ResultOwnership = .borrowed,
/// Providers mark allocated response fields as `owned`; test and embedded
/// providers may return stable borrowed fields instead.
pub fn deinit(self: *Result, alloc: Allocator) void {
if (self.ownership == .owned) {
if (self.err_body) |body| alloc.free(body);
if (self.completion.content) |content| alloc.free(@constCast(content));
if (self.completion.generation_id) |id| alloc.free(@constCast(id));
if (self.completion.billing) |billing| alloc.free(@constCast(billing.model));
types.freeToolCallSlice(alloc, @constCast(self.completion.tool_calls));
if (self.completion.provider_failure_detail) |detail| alloc.free(@constCast(detail));
if (self.failure_schema) |schema| alloc.free(schema);
if (self.failure_request_shape) |shape| alloc.free(shape);
}
const status = self.status;
self.* = .{ .status = status };
}
};
pub const StreamFn = *const fn (
context: ?*anyopaque,
alloc: Allocator,
request: Request,
) anyerror!Result;
pub const BuildFn = *const fn (
context: ?*anyopaque,
alloc: Allocator,
request: BuildRequest,
) anyerror![]u8;
pub const Provider = struct {
/// When set, context must remain valid until every in-flight `stream` returns.
context: ?*anyopaque = null,
build_fn: BuildFn = unavailableBuild,
stream_fn: StreamFn,
/// The returned request bytes are owned by `alloc`.
pub fn build(self: Provider, alloc: Allocator, request: BuildRequest) ![]u8 {
return self.build_fn(self.context, alloc, request);
}
pub fn stream(self: Provider, alloc: Allocator, request: Request) !Result {
return self.stream_fn(self.context, alloc, request);
}
};
fn unavailableBuild(_: ?*anyopaque, _: Allocator, _: BuildRequest) anyerror![]u8 {
return error.AgentStreamProviderUnavailable;
}
fn unavailableStream(_: ?*anyopaque, _: Allocator, _: Request) anyerror!Result {
return error.AgentStreamProviderUnavailable;
}
pub const unavailable_provider = Provider{
.stream_fn = unavailableStream,
};
test "stream provider dispatches request and preserves ordered callbacks" {
const Fake = struct {
calls: usize = 0,
attempt_owner: ?ProviderAttemptOwner = null,
fn stream(raw: ?*anyopaque, _: Allocator, request: Request) anyerror!Result {
const self: *@This() = @ptrCast(@alignCast(raw.?));
self.calls += 1;
self.attempt_owner = request.provider_attempt_owner;
request.on_content_chunk(request.callback_ctx, "first");
request.on_reasoning_chunk.?(request.callback_ctx, "second");
return .{
.status = .ok,
.completion = .{ .content = "done" },
.retry_after_seconds = 17,
};
}
};
const Capture = struct {
chunks: std.ArrayList(u8) = .empty,
failed: bool = false,
fn append(raw: *anyopaque, chunk: []const u8) void {
const self: *@This() = @ptrCast(@alignCast(raw));
self.chunks.appendSlice(std.testing.allocator, chunk) catch {
self.failed = true;
};
}
};
var fake: Fake = .{};
var capture: Capture = .{};
defer capture.chunks.deinit(std.testing.allocator);
var delivery = DeliveryCertainty.init();
var attempt_evidence: AttemptEvidence = .{};
var cancelled = std.atomic.Value(bool).init(false);
var result = try (Provider{
.context = &fake,
.stream_fn = Fake.stream,
}).stream(std.testing.allocator, .{
.api_key = "key",
.team = null,
.model = "model",
.retry_count = 1,
.chat_url = "https://example.test/chat",
.payload = "{}",
.trace_ctx = .{},
.content_capture_limit = null,
.delivery = &delivery,
.attempt_evidence = &attempt_evidence,
.callback_ctx = &capture,
.on_content_chunk = Capture.append,
.on_tool_start = null,
.on_reasoning_chunk = Capture.append,
.cancel_flag = &cancelled,
.provider_attempt_owner = .agent,
});
defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(usize, 1), fake.calls);
try std.testing.expectEqual(ProviderAttemptOwner.agent, fake.attempt_owner.?);
try std.testing.expect(!capture.failed);
try std.testing.expectEqualStrings("firstsecond", capture.chunks.items);
try std.testing.expectEqualStrings("done", result.completion.content.?);
try std.testing.expectEqual(@as(?u64, 17), result.retry_after_seconds);
}