const std = @import("std");
pub const PermissionMode = enum {
strict,
standard,
acceptEdits,
unrestricted,
pub fn allowsBash(self: PermissionMode) bool {
return self == .acceptEdits or self == .unrestricted;
}
};
pub const Config = struct {
api_base_url: []const u8,
api_key: []const u8,
permission_mode: PermissionMode = .standard,
enable_client_tools: bool = false,
agent_id: ?[]const u8 = null,
acting_user_id: ?[]const u8 = null,
/// External abort signal (e.g. `abort_message`). When set, in-flight HTTP
/// and SSE work returns `error.Cancelled` within one poll interval.
cancel_token: ?*const std.atomic.Value(bool) = null,
};
pub const cancel_poll_interval_ms: i64 = 20;
pub fn isCancelled(token: ?*const std.atomic.Value(bool)) bool {
return if (token) |t| t.load(.acquire) else false;
}
pub const InboundTurn = struct {
request_id: ?[]const u8,
conversation_id: []const u8,
kind: Kind,
messages: std.json.Value,
agent_id: ?[]const u8 = null,
acting_user_id: ?[]const u8 = null,
teleport_id: ?[]const u8 = null,
client_message_id: ?[]const u8 = null,
pub const Kind = enum { create_message, teleport_continue };
};
pub const PendingToolCall = struct {
tool_call_id: []u8,
name: []u8,
arguments: []u8,
message_id: ?[]u8 = null,
pub fn deinit(self: PendingToolCall, allocator: std.mem.Allocator) void {
allocator.free(self.tool_call_id);
allocator.free(self.name);
allocator.free(self.arguments);
if (self.message_id) |message_id| allocator.free(message_id);
}
};
pub const ApprovalStatus = enum { success, @"error" };
pub const ApprovalResult = struct {
tool_call_id: []const u8,
status: ApprovalStatus,
tool_return: []const u8,
};
pub const Result = union(enum) {
completed: void,
requires_approval: []PendingToolCall,
api_error: struct { status: std.http.Status, body: []u8 },
};
pub const ChunkCallback = *const fn (ctx: ?*anyopaque, raw_chunk: []const u8) anyerror!void;
pub const max_sse_line_bytes = 1024 * 1024;
pub const max_sse_event_bytes = 4 * 1024 * 1024;
const max_api_error_body_bytes = 1024 * 1024;
const response_read_buffer_bytes = 16 * 1024;
pub const sse_inactivity_timeout_ms: i64 = 90_000;
pub const http_operation_timeout_ms: i64 = 90_000;
const StreamState = struct {
allocator: std.mem.Allocator,
stop_reason: ?[]u8 = null,
pending_tool_calls: std.ArrayList(PendingToolCall) = .empty,
fn deinit(self: *StreamState) void {
if (self.stop_reason) |reason| self.allocator.free(reason);
for (self.pending_tool_calls.items) |call| call.deinit(self.allocator);
self.pending_tool_calls.deinit(self.allocator);
}
};
pub fn parseInboundInput(allocator: std.mem.Allocator, frame: []const u8) !std.json.Parsed(InboundTurn) {
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, frame, .{});
errdefer parsed.deinit();
const obj = switch (parsed.value) {
.object => |object| object,
else => return error.InvalidInboundFrame,
};
const typ = jsonString(obj.get("type")) orelse return error.InvalidInboundFrame;
if (!std.mem.eql(u8, typ, "input")) return error.NotInputFrame;
const runtime = jsonObject(obj.get("runtime")) orelse return error.InvalidInboundFrame;
const conversation_id = jsonString(runtime.get("conversation_id")) orelse return error.MissingConversationId;
const payload = jsonObject(obj.get("payload")) orelse return error.InvalidInboundFrame;
const kind_string = jsonString(payload.get("kind")) orelse return error.InvalidInboundFrame;
if (std.mem.eql(u8, kind_string, "create_message")) {
return .{
.arena = parsed.arena,
.value = .{
.request_id = jsonString(obj.get("request_id")),
.conversation_id = conversation_id,
.kind = .create_message,
.messages = payload.get("messages") orelse return error.MissingMessages,
.agent_id = jsonString(runtime.get("agent_id")),
.acting_user_id = jsonString(runtime.get("acting_user_id")),
.teleport_id = null,
.client_message_id = firstClientMessageId(payload.get("messages")),
},
};
}
if (std.mem.eql(u8, kind_string, "teleport_continue")) {
const continuation = jsonObject(payload.get("continuation"));
const messages = if (continuation) |c|
c.get("approvals") orelse std.json.Value{ .array = std.json.Array.init(parsed.arena.allocator()) }
else
std.json.Value{ .array = std.json.Array.init(parsed.arena.allocator()) };
return .{
.arena = parsed.arena,
.value = .{
.request_id = jsonString(obj.get("request_id")),
.conversation_id = conversation_id,
.kind = .teleport_continue,
.messages = messages,
.agent_id = jsonString(runtime.get("agent_id")),
.acting_user_id = jsonString(runtime.get("acting_user_id")),
.teleport_id = jsonString(payload.get("teleport_id")),
.client_message_id = null,
},
};
}
return error.UnsupportedInputKind;
}
pub fn buildRequestBody(allocator: std.mem.Allocator, inbound: InboundTurn, config: Config) ![]u8 {
var out: std.Io.Writer.Allocating = .init(allocator);
defer out.deinit();
try out.writer.writeAll("{\"messages\":");
if (inbound.kind == .teleport_continue) {
try out.writer.writeByte('[');
if (inbound.messages == .array and inbound.messages.array.items.len > 0) {
try out.writer.writeAll("{\"type\":\"approval\",\"approvals\":");
try std.json.Stringify.value(inbound.messages, .{}, &out.writer);
try out.writer.writeAll(",\"otid\":");
try writeJsonString(&out.writer, inbound.teleport_id orelse "portal-teleport-continuation");
try out.writer.writeAll("},");
}
try out.writer.writeAll("{\"role\":\"system\",\"content\":\"Teleportation to this environment is complete. Continue the existing task from this environment now.\",\"otid\":");
const teleport_id = inbound.teleport_id orelse "portal-teleport-continuation";
var continue_otid: [512]u8 = undefined;
try writeJsonString(&out.writer, try std.fmt.bufPrint(&continue_otid, "{s}:continue", .{teleport_id}));
try out.writer.writeAll("}]");
} else {
try std.json.Stringify.value(inbound.messages, .{}, &out.writer);
}
try out.writer.writeAll(",\"streaming\":true,\"stream_tokens\":true,\"include_pings\":true,\"background\":true,\"include_compaction_messages\":true");
if (config.agent_id orelse inbound.agent_id) |agent_id| {
try out.writer.writeAll(",\"agent_id\":");
try writeJsonString(&out.writer, agent_id);
}
try out.writer.writeAll(",\"client_tools\":");
if (config.enable_client_tools) try writeClientTools(&out.writer, config.permission_mode) else try out.writer.writeAll("[]");
try out.writer.writeAll("}");
return out.toOwnedSlice();
}
pub fn buildMessagesUrl(allocator: std.mem.Allocator, api_base_url: []const u8, conversation_id: []const u8) ![]u8 {
const base = std.mem.trimEnd(u8, api_base_url, "/");
return std.fmt.allocPrint(allocator, "{s}/v1/conversations/{s}/messages", .{ base, conversation_id });
}
pub fn runTurnStreaming(
allocator: std.mem.Allocator,
io: std.Io,
config: Config,
inbound: InboundTurn,
callback: ChunkCallback,
callback_ctx: ?*anyopaque,
) !Result {
const body = try buildRequestBody(allocator, inbound, config);
defer allocator.free(body);
return runBodyStreaming(allocator, io, config, inbound.conversation_id, body, callback, callback_ctx);
}
pub fn runApprovalContinuationStreaming(
allocator: std.mem.Allocator,
io: std.Io,
config: Config,
conversation_id: []const u8,
approvals: []const ApprovalResult,
otid: []const u8,
callback: ChunkCallback,
callback_ctx: ?*anyopaque,
) !Result {
var body_writer: std.Io.Writer.Allocating = .init(allocator);
defer body_writer.deinit();
try body_writer.writer.writeAll("{\"messages\":[{\"type\":\"approval\",\"approvals\":[");
for (approvals, 0..) |approval, i| {
if (i != 0) try body_writer.writer.writeByte(',');
try body_writer.writer.writeAll("{\"type\":\"tool\",\"tool_call_id\":");
try writeJsonString(&body_writer.writer, approval.tool_call_id);
try body_writer.writer.writeAll(",\"status\":");
try writeJsonString(&body_writer.writer, @tagName(approval.status));
try body_writer.writer.writeAll(",\"tool_return\":");
try writeJsonString(&body_writer.writer, approval.tool_return);
try body_writer.writer.writeByte('}');
}
try body_writer.writer.writeAll("],\"otid\":");
try writeJsonString(&body_writer.writer, otid);
try body_writer.writer.writeAll("}],\"streaming\":true,\"stream_tokens\":true,\"include_pings\":true,\"background\":true,\"include_compaction_messages\":true");
if (config.agent_id) |agent_id| {
try body_writer.writer.writeAll(",\"agent_id\":");
try writeJsonString(&body_writer.writer, agent_id);
}
try body_writer.writer.writeAll(",\"client_tools\":");
if (config.enable_client_tools) try writeClientTools(&body_writer.writer, config.permission_mode) else try body_writer.writer.writeAll("[]");
try body_writer.writer.writeByte('}');
const body = try body_writer.toOwnedSlice();
defer allocator.free(body);
return runBodyStreaming(allocator, io, config, conversation_id, body, callback, callback_ctx);
}
fn runBodyStreaming(
allocator: std.mem.Allocator,
io: std.Io,
config: Config,
conversation_id: []const u8,
body: []u8,
callback: ChunkCallback,
callback_ctx: ?*anyopaque,
) !Result {
const url = try buildMessagesUrl(allocator, config.api_base_url, conversation_id);
defer allocator.free(url);
const auth = try std.fmt.allocPrint(allocator, "Bearer {s}", .{config.api_key});
defer allocator.free(auth);
var client: std.http.Client = .{ .allocator = allocator, .io = io };
defer client.deinit();
var headers: [5]std.http.Header = undefined;
headers[0] = .{ .name = "Authorization", .value = auth };
headers[1] = .{ .name = "Content-Type", .value = "application/json" };
headers[2] = .{ .name = "Accept", .value = "text/event-stream" };
headers[3] = .{ .name = "X-Letta-Source", .value = "letta-code" };
var header_count: usize = 4;
if (config.acting_user_id) |acting_user_id| {
headers[header_count] = .{ .name = "X-Letta-Acting-User-Id", .value = acting_user_id };
header_count += 1;
}
if (isCancelled(config.cancel_token)) return error.Cancelled;
var request = try requestWithInactivityTimeout(&client, io, .POST, try std.Uri.parse(url), .{
.redirect_behavior = .unhandled,
.extra_headers = headers[0..header_count],
.keep_alive = false,
.headers = .{ .accept_encoding = .{ .override = "identity" } },
}, http_operation_timeout_ms, config.cancel_token);
defer request.deinit();
try sendBodyWithInactivityTimeout(io, &request, body, http_operation_timeout_ms, config.cancel_token);
var response = try receiveHeadWithInactivityTimeout(io, &request, http_operation_timeout_ms, config.cancel_token);
const status = response.head.status;
if (response.head.content_encoding != .identity) return error.UnsupportedCompressionMethod;
var transfer_buffer: [response_read_buffer_bytes]u8 = undefined;
const reader = response.reader(&transfer_buffer);
if (@intFromEnum(status) < 200 or @intFromEnum(status) >= 300) {
var response_body: std.ArrayList(u8) = .empty;
defer response_body.deinit(allocator);
var read_buffer: [response_read_buffer_bytes]u8 = undefined;
while (true) {
var vectors = [_][]u8{&read_buffer};
const bytes_read = readApiErrorBodyWithInactivityTimeout(io, reader, &vectors, http_operation_timeout_ms, config.cancel_token) catch |err| switch (err) {
error.EndOfStream => break,
error.ReadFailed => return response.bodyErr() orelse err,
else => return err,
};
if (bytes_read == 0) continue;
const remaining_capacity = max_api_error_body_bytes - response_body.items.len;
try response_body.appendSlice(allocator, read_buffer[0..@min(bytes_read, remaining_capacity)]);
}
const owned_body = try response_body.toOwnedSlice(allocator);
return .{ .api_error = .{ .status = status, .body = owned_body } };
}
var parser = SseParser.init(allocator, callback, callback_ctx);
defer parser.deinit();
var read_buffer: [response_read_buffer_bytes]u8 = undefined;
while (true) {
var vectors = [_][]u8{&read_buffer};
if (isCancelled(config.cancel_token)) return error.Cancelled;
const bytes_read = readWithInactivityTimeout(io, reader, &vectors, sse_inactivity_timeout_ms, config.cancel_token) catch |err| switch (err) {
error.EndOfStream => break,
error.ReadFailed => return response.bodyErr() orelse err,
else => return err,
};
if (bytes_read == 0) continue;
try parser.feed(read_buffer[0..bytes_read]);
}
if (isCancelled(config.cancel_token)) return error.Cancelled;
return parser.finish();
}
fn runWithInactivityTimeout(
comptime ReturnType: type,
io: std.Io,
timeout_ms: i64,
comptime operation: anytype,
operation_args: anytype,
comptime timeout_error: anyerror,
cancel_token: ?*const std.atomic.Value(bool),
) !ReturnType {
const OperationResult = @typeInfo(@TypeOf(operation)).@"fn".return_type.?;
const Selection = union(enum) {
operation: OperationResult,
timeout: std.Io.Cancelable!void,
cancelled: anyerror!void,
};
const Tasks = struct {
fn timeout(target_io: std.Io, milliseconds: i64) std.Io.Cancelable!void {
return std.Io.sleep(target_io, .fromMilliseconds(milliseconds), .awake);
}
fn waitForCancel(target_io: std.Io, token: *const std.atomic.Value(bool)) anyerror!void {
while (!token.load(.acquire)) {
try std.Io.sleep(target_io, .fromMilliseconds(cancel_poll_interval_ms), .awake);
}
return error.Cancelled;
}
};
if (isCancelled(cancel_token)) return error.Cancelled;
var results: [3]Selection = undefined;
var select = std.Io.Select(Selection).init(io, &results);
select.async(.operation, operation, operation_args);
select.async(.timeout, Tasks.timeout, .{ io, timeout_ms });
if (cancel_token) |token| select.async(.cancelled, Tasks.waitForCancel, .{ io, token });
defer select.cancelDiscard();
const first = try select.await();
return switch (first) {
.operation => |operation_result| try operation_result,
.timeout => |timeout_result| {
try timeout_result;
return timeout_error;
},
.cancelled => |cancel_result| {
try cancel_result;
return error.Cancelled;
},
};
}
fn requestWithInactivityTimeout(
client: *std.http.Client,
io: std.Io,
method: std.http.Method,
uri: std.Uri,
options: std.http.Client.RequestOptions,
timeout_ms: i64,
cancel_token: ?*const std.atomic.Value(bool),
) !std.http.Client.Request {
const Slot = struct { request: ?std.http.Client.Request = null };
const Tasks = struct {
fn create(slot: *Slot, target: *std.http.Client, request_method: std.http.Method, request_uri: std.Uri, request_options: std.http.Client.RequestOptions) anyerror!void {
slot.request = try target.request(request_method, request_uri, request_options);
}
};
var slot = Slot{};
runWithInactivityTimeout(void, io, timeout_ms, Tasks.create, .{ &slot, client, method, uri, options }, error.HttpOperationTimeout, cancel_token) catch |err| {
if (slot.request) |*request| request.deinit();
return err;
};
const request = slot.request orelse return error.HttpRequestMissing;
slot.request = null;
return request;
}
fn sendBodyWithInactivityTimeout(io: std.Io, request: *std.http.Client.Request, body: []u8, timeout_ms: i64, cancel_token: ?*const std.atomic.Value(bool)) !void {
const Tasks = struct {
fn send(target: *std.http.Client.Request, request_body: []u8) anyerror!void {
return target.sendBodyComplete(request_body);
}
};
return runWithInactivityTimeout(void, io, timeout_ms, Tasks.send, .{ request, body }, error.HttpOperationTimeout, cancel_token);
}
fn receiveHeadWithInactivityTimeout(io: std.Io, request: *std.http.Client.Request, timeout_ms: i64, cancel_token: ?*const std.atomic.Value(bool)) !std.http.Client.Response {
const Tasks = struct {
fn receiveHead(target: *std.http.Client.Request) std.http.Client.Request.ReceiveHeadError!std.http.Client.Response {
return target.receiveHead(&.{});
}
};
return runWithInactivityTimeout(std.http.Client.Response, io, timeout_ms, Tasks.receiveHead, .{request}, error.HttpOperationTimeout, cancel_token);
}
fn readWithTimeout(io: std.Io, reader: *std.Io.Reader, vectors: [][]u8, timeout_ms: i64, comptime timeout_error: anyerror, cancel_token: ?*const std.atomic.Value(bool)) !usize {
const Tasks = struct {
fn read(target: *std.Io.Reader, buffers: [][]u8) anyerror!usize {
return target.readVec(buffers);
}
};
return runWithInactivityTimeout(usize, io, timeout_ms, Tasks.read, .{ reader, vectors }, timeout_error, cancel_token);
}
fn readWithInactivityTimeout(io: std.Io, reader: *std.Io.Reader, vectors: [][]u8, timeout_ms: i64, cancel_token: ?*const std.atomic.Value(bool)) !usize {
return readWithTimeout(io, reader, vectors, timeout_ms, error.SseInactivityTimeout, cancel_token);
}
fn readApiErrorBodyWithInactivityTimeout(io: std.Io, reader: *std.Io.Reader, vectors: [][]u8, timeout_ms: i64, cancel_token: ?*const std.atomic.Value(bool)) !usize {
return readWithTimeout(io, reader, vectors, timeout_ms, error.HttpOperationTimeout, cancel_token);
}
pub fn collectTurnFromSse(allocator: std.mem.Allocator, sse_body: []const u8) !struct { result: Result, chunks: [][]u8 } {
var chunks = std.ArrayList([]u8).empty;
errdefer {
for (chunks.items) |chunk| allocator.free(chunk);
chunks.deinit(allocator);
}
const Context = struct {
allocator: std.mem.Allocator,
chunks: *std.ArrayList([]u8),
};
var context = Context{ .allocator = allocator, .chunks = &chunks };
const cb = struct {
fn append(ctx: ?*anyopaque, raw_chunk: []const u8) !void {
const context_ptr: *Context = @ptrCast(@alignCast(ctx.?));
try context_ptr.chunks.append(context_ptr.allocator, try context_ptr.allocator.dupe(u8, raw_chunk));
}
}.append;
const result = try parseSseAndEmit(allocator, sse_body, cb, &context);
return .{ .result = result, .chunks = try chunks.toOwnedSlice(allocator) };
}
pub fn parseSseAndEmit(allocator: std.mem.Allocator, sse_body: []const u8, callback: ChunkCallback, callback_ctx: ?*anyopaque) !Result {
var parser = SseParser.init(allocator, callback, callback_ctx);
defer parser.deinit();
try parser.feed(sse_body);
return parser.finish();
}
const SseParser = struct {
allocator: std.mem.Allocator,
callback: ChunkCallback,
callback_ctx: ?*anyopaque,
line: std.ArrayList(u8) = .empty,
event_data: std.ArrayList(u8) = .empty,
has_data: bool = false,
stream_state: StreamState,
finished: bool = false,
fn init(allocator: std.mem.Allocator, callback: ChunkCallback, callback_ctx: ?*anyopaque) SseParser {
return .{
.allocator = allocator,
.callback = callback,
.callback_ctx = callback_ctx,
.stream_state = .{ .allocator = allocator },
};
}
fn deinit(self: *SseParser) void {
self.line.deinit(self.allocator);
self.event_data.deinit(self.allocator);
self.stream_state.deinit();
}
fn feed(self: *SseParser, bytes: []const u8) !void {
if (self.finished) return error.SseParserFinished;
var remaining = bytes;
while (std.mem.findScalar(u8, remaining, '\n')) |newline| {
try self.appendLineBytes(remaining[0..newline]);
try self.consumeLine();
remaining = remaining[newline + 1 ..];
}
try self.appendLineBytes(remaining);
}
fn finish(self: *SseParser) !Result {
if (self.finished) return error.SseParserFinished;
self.finished = true;
if (self.line.items.len > 0) try self.consumeLine();
try self.dispatchEvent();
if (self.stream_state.stop_reason) |reason| {
if (std.mem.eql(u8, reason, "requires_approval") and self.stream_state.pending_tool_calls.items.len > 0) {
self.allocator.free(reason);
self.stream_state.stop_reason = null;
return .{ .requires_approval = try self.stream_state.pending_tool_calls.toOwnedSlice(self.allocator) };
}
}
return .{ .completed = {} };
}
fn appendLineBytes(self: *SseParser, bytes: []const u8) !void {
if (self.line.items.len + bytes.len > max_sse_line_bytes) return error.SseLineTooLarge;
try self.line.appendSlice(self.allocator, bytes);
}
fn consumeLine(self: *SseParser) !void {
const line = std.mem.trimEnd(u8, self.line.items, "\r");
if (line.len == 0) {
try self.dispatchEvent();
} else if (std.mem.startsWith(u8, line, "data:")) {
var value = line[5..];
if (value.len > 0 and value[0] == ' ') value = value[1..];
const separator_len: usize = if (self.has_data) 1 else 0;
if (self.event_data.items.len + separator_len + value.len > max_sse_event_bytes) return error.SseEventTooLarge;
if (self.has_data) try self.event_data.append(self.allocator, '\n');
try self.event_data.appendSlice(self.allocator, value);
self.has_data = true;
}
self.line.clearRetainingCapacity();
}
fn dispatchEvent(self: *SseParser) !void {
if (!self.has_data) return;
self.has_data = false;
defer self.event_data.clearRetainingCapacity();
const raw_chunk = self.event_data.items;
if (std.mem.eql(u8, raw_chunk, "[DONE]")) return;
var parsed = std.json.parseFromSlice(std.json.Value, self.allocator, raw_chunk, .{}) catch return error.InvalidSseJson;
defer parsed.deinit();
try absorbChunk(self.allocator, parsed.value, &self.stream_state);
try self.callback(self.callback_ctx, raw_chunk);
}
};
fn absorbChunk(allocator: std.mem.Allocator, value: std.json.Value, stream_state: *StreamState) !void {
const obj = jsonObject(value) orelse return;
const message_type = jsonString(obj.get("message_type"));
if (message_type) |mt| {
if (std.mem.eql(u8, mt, "stop_reason")) {
if (stream_state.stop_reason) |old| allocator.free(old);
stream_state.stop_reason = try allocator.dupe(u8, jsonString(obj.get("stop_reason")) orelse "");
return;
}
if (std.mem.eql(u8, mt, "approval_request_message")) {
const message_id = jsonString(obj.get("id"));
if (obj.get("tool_calls")) |tool_calls_value| {
if (tool_calls_value == .array) {
for (tool_calls_value.array.items) |tool_call_value| {
try appendPendingToolCall(allocator, stream_state, tool_call_value, message_id);
}
}
}
if (obj.get("tool_call")) |tool_call_value| {
try appendPendingToolCall(allocator, stream_state, tool_call_value, message_id);
}
}
}
}
fn appendPendingToolCall(allocator: std.mem.Allocator, stream_state: *StreamState, tool_call_value: std.json.Value, message_id: ?[]const u8) !void {
const tool_call = jsonObject(tool_call_value) orelse return;
const id = jsonString(tool_call.get("tool_call_id")) orelse return;
const name = jsonString(tool_call.get("name")) orelse "";
const arguments = jsonString(tool_call.get("arguments")) orelse "";
for (stream_state.pending_tool_calls.items) |*existing| {
if (std.mem.eql(u8, existing.tool_call_id, id)) {
if (name.len > 0) {
allocator.free(existing.name);
existing.name = try allocator.dupe(u8, name);
}
if (arguments.len > 0) {
const old = existing.arguments;
existing.arguments = try std.fmt.allocPrint(allocator, "{s}{s}", .{ old, arguments });
allocator.free(old);
}
if (existing.message_id == null and message_id != null) existing.message_id = try allocator.dupe(u8, message_id.?);
return;
}
}
try stream_state.pending_tool_calls.append(allocator, .{
.tool_call_id = try allocator.dupe(u8, id),
.name = try allocator.dupe(u8, name),
.arguments = try allocator.dupe(u8, arguments),
.message_id = if (message_id) |mid| try allocator.dupe(u8, mid) else null,
});
}
fn writeClientTools(writer: *std.Io.Writer, permission_mode: PermissionMode) !void {
try writer.writeAll("[");
try writeTool(writer, "Read", "Read a file from the local filesystem", "{\"type\":\"object\",\"properties\":{\"file_path\":{\"type\":\"string\"},\"offset\":{\"type\":\"integer\"},\"limit\":{\"type\":\"integer\"}},\"required\":[\"file_path\"]}");
try writer.writeAll(",");
try writeTool(writer, "LS", "List directory entries", "{\"type\":\"object\",\"properties\":{\"path\":{\"type\":\"string\"}},\"required\":[\"path\"]}");
try writer.writeAll(",");
try writeTool(writer, "Glob", "Find files by glob pattern", "{\"type\":\"object\",\"properties\":{\"pattern\":{\"type\":\"string\"},\"path\":{\"type\":\"string\"}},\"required\":[\"pattern\"]}");
try writer.writeAll(",");
try writeTool(writer, "Grep", "Search file contents by pattern", "{\"type\":\"object\",\"properties\":{\"pattern\":{\"type\":\"string\"},\"path\":{\"type\":\"string\"},\"glob\":{\"type\":\"string\"}},\"required\":[\"pattern\"]}");
if (permission_mode.allowsBash()) {
try writer.writeAll(",");
try writeTool(writer, "Bash", "Execute a shell command when permission mode allows it", "{\"type\":\"object\",\"properties\":{\"command\":{\"type\":\"string\"},\"description\":{\"type\":\"string\"},\"timeout\":{\"type\":\"integer\"},\"run_in_background\":{\"type\":\"boolean\"}},\"required\":[\"command\",\"description\"]}");
}
try writer.writeAll("]");
}
fn writeTool(writer: *std.Io.Writer, name: []const u8, description: []const u8, parameters_json: []const u8) !void {
try writer.writeAll("{\"name\":");
try writeJsonString(writer, name);
try writer.writeAll(",\"description\":");
try writeJsonString(writer, description);
try writer.writeAll(",\"parameters\":");
try writer.writeAll(parameters_json);
try writer.writeAll("}");
}
fn writeJsonString(writer: *std.Io.Writer, value: []const u8) !void {
try std.json.Stringify.value(value, .{}, writer);
}
fn firstClientMessageId(value: ?std.json.Value) ?[]const u8 {
const messages = value orelse return null;
if (messages != .array) return null;
for (messages.array.items) |message| {
if (message != .object or message.object.get("content") == null) continue;
if (jsonString(message.object.get("client_message_id"))) |client_message_id| {
if (client_message_id.len > 0) return client_message_id;
}
}
return null;
}
fn jsonString(value: ?std.json.Value) ?[]const u8 {
const v = value orelse return null;
return switch (v) {
.string => |s| s,
else => null,
};
}
fn jsonObject(value: ?std.json.Value) ?std.json.ObjectMap {
const v = value orelse return null;
return switch (v) {
.object => |object| object,
else => null,
};
}
pub fn freePendingToolCalls(allocator: std.mem.Allocator, calls: []PendingToolCall) void {
for (calls) |call| call.deinit(allocator);
allocator.free(calls);
}
pub fn freeChunks(allocator: std.mem.Allocator, chunks: [][]u8) void {
for (chunks) |chunk| allocator.free(chunk);
allocator.free(chunks);
}
test "parse inbound create_message extracts runtime and messages" {
const frame = "{\"type\":\"input\",\"request_id\":\"req-1\",\"runtime\":{\"conversation_id\":\"conv-1\",\"agent_id\":\"agent-1\",\"acting_user_id\":\"user-1\"},\"payload\":{\"kind\":\"create_message\",\"messages\":[{\"type\":\"message\",\"role\":\"user\",\"content\":\"hi\"}]}}";
var parsed = try parseInboundInput(std.testing.allocator, frame);
defer parsed.deinit();
try std.testing.expectEqualStrings("req-1", parsed.value.request_id.?);
try std.testing.expectEqualStrings("conv-1", parsed.value.conversation_id);
try std.testing.expect(parsed.value.kind == .create_message);
try std.testing.expect(parsed.value.messages == .array);
try std.testing.expectEqual(@as(usize, 1), parsed.value.messages.array.items.len);
try std.testing.expectEqualStrings("user-1", parsed.value.acting_user_id.?);
}
test "request body includes required streaming flags and gated Bash" {
const frame = "{\"type\":\"input\",\"runtime\":{\"conversation_id\":\"conv-1\"},\"payload\":{\"kind\":\"create_message\",\"messages\":[{\"type\":\"message\",\"role\":\"user\",\"content\":\"hi\"}]}}";
var parsed = try parseInboundInput(std.testing.allocator, frame);
defer parsed.deinit();
const body_standard = try buildRequestBody(std.testing.allocator, parsed.value, .{ .api_base_url = "https://api.letta.com", .api_key = "test", .permission_mode = .standard, .enable_client_tools = true });
defer std.testing.allocator.free(body_standard);
try std.testing.expect(std.mem.indexOf(u8, body_standard, "\"streaming\":true") != null);
try std.testing.expect(std.mem.indexOf(u8, body_standard, "\"include_compaction_messages\":true") != null);
try std.testing.expect(std.mem.indexOf(u8, body_standard, "\"name\":\"Bash\"") == null);
const body_unrestricted = try buildRequestBody(std.testing.allocator, parsed.value, .{ .api_base_url = "https://api.letta.com", .api_key = "test", .permission_mode = .unrestricted, .enable_client_tools = true });
defer std.testing.allocator.free(body_unrestricted);
try std.testing.expect(std.mem.indexOf(u8, body_unrestricted, "\"name\":\"Bash\"") != null);
}
test "teleport continuation wraps tool results in approval message" {
const frame = "{\"type\":\"input\",\"runtime\":{\"conversation_id\":\"conv-1\",\"agent_id\":\"agent-1\"},\"payload\":{\"kind\":\"teleport_continue\",\"teleport_id\":\"teleport-1\",\"continuation\":{\"approvals\":[{\"type\":\"tool\",\"tool_call_id\":\"call-1\",\"status\":\"success\",\"tool_return\":\"done\"}]}}}";
var parsed = try parseInboundInput(std.testing.allocator, frame);
defer parsed.deinit();
const body = try buildRequestBody(std.testing.allocator, parsed.value, .{ .api_base_url = "https://api.letta.com", .api_key = "test" });
defer std.testing.allocator.free(body);
try std.testing.expect(std.mem.indexOf(u8, body, "\"type\":\"approval\"") != null);
try std.testing.expect(std.mem.indexOf(u8, body, "\"approvals\":[{\"type\":\"tool\"") != null);
try std.testing.expect(std.mem.indexOf(u8, body, "\"otid\":\"teleport-1\"") != null);
}
test "empty teleport continuation includes exact completion reminder" {
const frame = "{\"type\":\"input\",\"runtime\":{\"conversation_id\":\"conv-1\",\"agent_id\":\"agent-1\"},\"payload\":{\"kind\":\"teleport_continue\",\"teleport_id\":\"teleport-empty\"}}";
var parsed = try parseInboundInput(std.testing.allocator, frame);
defer parsed.deinit();
const body = try buildRequestBody(std.testing.allocator, parsed.value, .{ .api_base_url = "https://api.letta.com", .api_key = "test" });
defer std.testing.allocator.free(body);
try std.testing.expect(std.mem.indexOf(u8, body, "\"type\":\"approval\"") == null);
try std.testing.expect(std.mem.indexOf(u8, body, "Teleportation to this environment is complete. Continue the existing task from this environment now.") != null);
try std.testing.expect(std.mem.indexOf(u8, body, "\"otid\":\"teleport-empty:continue\"") != null);
}
const TimeoutTestReader = struct {
reader: std.Io.Reader,
io: std.Io,
exited: ?*std.atomic.Value(bool) = null,
const vtable = std.Io.Reader.VTable{ .stream = stream, .readVec = readVec };
fn init(io: std.Io, buffer: []u8, exited: ?*std.atomic.Value(bool)) @This() {
return .{
.reader = .{ .vtable = &vtable, .buffer = buffer, .seek = 0, .end = 0 },
.io = io,
.exited = exited,
};
}
fn wait(self: *@This()) std.Io.Reader.Error!usize {
std.Io.sleep(self.io, .fromSeconds(30), .awake) catch {
if (self.exited) |exited| exited.store(true, .release);
return error.ReadFailed;
};
if (self.exited) |exited| exited.store(true, .release);
return error.EndOfStream;
}
fn stream(reader: *std.Io.Reader, _: *std.Io.Writer, _: std.Io.Limit) std.Io.Reader.StreamError!usize {
const self: *@This() = @fieldParentPtr("reader", reader);
return self.wait() catch |err| return err;
}
fn readVec(reader: *std.Io.Reader, _: [][]u8) std.Io.Reader.Error!usize {
const self: *@This() = @fieldParentPtr("reader", reader);
return self.wait();
}
};
test "SSE inactivity timeout cancels a stalled read" {
var internal_buffer: [1]u8 = undefined;
var stalled = TimeoutTestReader.init(std.testing.io, &internal_buffer, null);
var output: [1]u8 = undefined;
var vectors = [_][]u8{&output};
try std.testing.expectError(error.SseInactivityTimeout, readWithInactivityTimeout(std.testing.io, &stalled.reader, &vectors, 10, null));
}
test "HTTP request creation timeout cancels stalled operation" {
const Tasks = struct {
fn stalled(io: std.Io, exited: *std.atomic.Value(bool)) anyerror!usize {
std.Io.sleep(io, .fromSeconds(30), .awake) catch {
exited.store(true, .release);
return error.Canceled;
};
return 0;
}
};
var exited: std.atomic.Value(bool) = .init(false);
try std.testing.expectError(error.HttpOperationTimeout, runWithInactivityTimeout(usize, std.testing.io, 10, Tasks.stalled, .{ std.testing.io, &exited }, error.HttpOperationTimeout, null));
try std.testing.expect(exited.load(.acquire));
}
test "HTTP send body timeout cancels stalled operation" {
const Tasks = struct {
fn stalled(io: std.Io, exited: *std.atomic.Value(bool)) anyerror!void {
std.Io.sleep(io, .fromSeconds(30), .awake) catch {
exited.store(true, .release);
return error.Canceled;
};
}
};
var exited: std.atomic.Value(bool) = .init(false);
try std.testing.expectError(error.HttpOperationTimeout, runWithInactivityTimeout(void, std.testing.io, 10, Tasks.stalled, .{ std.testing.io, &exited }, error.HttpOperationTimeout, null));
try std.testing.expect(exited.load(.acquire));
}
test "HTTP error body inactivity timeout cancels stalled read" {
var internal_buffer: [1]u8 = undefined;
var stalled = TimeoutTestReader.init(std.testing.io, &internal_buffer, null);
var output: [1]u8 = undefined;
var vectors = [_][]u8{&output};
try std.testing.expectError(error.HttpOperationTimeout, readApiErrorBodyWithInactivityTimeout(std.testing.io, &stalled.reader, &vectors, 10, null));
}
test "cancel token aborts a stalled SSE read" {
var token: std.atomic.Value(bool) = .init(false);
const Tasks = struct {
fn cancelLater(io: std.Io, t: *std.atomic.Value(bool)) !void {
try std.Io.sleep(io, .fromMilliseconds(20), .awake);
t.store(true, .release);
}
};
var group: std.Io.Group = .init;
group.async(std.testing.io, Tasks.cancelLater, .{ std.testing.io, &token });
defer group.cancel(std.testing.io);
var internal_buffer: [1]u8 = undefined;
var stalled = TimeoutTestReader.init(std.testing.io, &internal_buffer, null);
var output: [1]u8 = undefined;
var vectors = [_][]u8{&output};
try std.testing.expectError(error.Cancelled, readWithInactivityTimeout(std.testing.io, &stalled.reader, &vectors, 30_000, &token));
}
test "cancel token aborts a stalled HTTP operation" {
var token: std.atomic.Value(bool) = .init(false);
const Tasks = struct {
fn stalled(io: std.Io, exited: *std.atomic.Value(bool)) anyerror!void {
std.Io.sleep(io, .fromSeconds(30), .awake) catch {
exited.store(true, .release);
return error.Canceled;
};
}
fn cancelLater(io: std.Io, t: *std.atomic.Value(bool)) !void {
try std.Io.sleep(io, .fromMilliseconds(20), .awake);
t.store(true, .release);
}
};
var exited: std.atomic.Value(bool) = .init(false);
var group: std.Io.Group = .init;
group.async(std.testing.io, Tasks.cancelLater, .{ std.testing.io, &token });
defer group.cancel(std.testing.io);
try std.testing.expectError(error.Cancelled, runWithInactivityTimeout(void, std.testing.io, 30_000, Tasks.stalled, .{ std.testing.io, &exited }, error.HttpOperationTimeout, &token));
}
test "absent cancel token preserves timeout behavior" {
const Tasks = struct {
fn stalled(io: std.Io, exited: *std.atomic.Value(bool)) anyerror!void {
std.Io.sleep(io, .fromSeconds(30), .awake) catch {
exited.store(true, .release);
return error.Canceled;
};
}
};
var exited: std.atomic.Value(bool) = .init(false);
try std.testing.expectError(error.HttpOperationTimeout, runWithInactivityTimeout(void, std.testing.io, 10, Tasks.stalled, .{ std.testing.io, &exited }, error.HttpOperationTimeout, null));
try std.testing.expect(exited.load(.acquire));
}
test "parent cancellation waits for nested Select cleanup" {
var exited: std.atomic.Value(bool) = .init(false);
var internal_buffer: [1]u8 = undefined;
var stalled = TimeoutTestReader.init(std.testing.io, &internal_buffer, &exited);
var output: [1]u8 = undefined;
var vectors = [_][]u8{&output};
const Selection = union(enum) { operation: anyerror!usize };
const Tasks = struct {
fn read(io: std.Io, reader: *std.Io.Reader, buffers: [][]u8) anyerror!usize {
return readWithInactivityTimeout(io, reader, buffers, 30_000, null);
}
};
var results: [1]Selection = undefined;
var parent = std.Io.Select(Selection).init(std.testing.io, &results);
parent.async(.operation, Tasks.read, .{ std.testing.io, &stalled.reader, &vectors });
parent.cancelDiscard();
try std.testing.expect(exited.load(.acquire));
}
test "SSE parser rejects malformed JSON before callback" {
var callback_count: usize = 0;
const cb = struct {
fn count(ctx: ?*anyopaque, _: []const u8) !void {
const count_ptr: *usize = @ptrCast(@alignCast(ctx.?));
count_ptr.* += 1;
}
}.count;
try std.testing.expectError(error.InvalidSseJson, parseSseAndEmit(std.testing.allocator, "data: {not-json}\n\n", cb, &callback_count));
try std.testing.expectEqual(@as(usize, 0), callback_count);
}
test "SSE parser emits chunks and surfaces approval stops" {
const sse =
"event: message\n" ++
"data: {\"message_type\":\"assistant_message\",\"content\":\"thinking\"}\n\n" ++
"data: {\"message_type\":\"approval_request_message\",\"id\":\"msg-1\",\"tool_calls\":[{\"tool_call_id\":\"call-1\",\"name\":\"Bash\",\"arguments\":\"pw\"}]}\n\n" ++
"data: {\"message_type\":\"approval_request_message\",\"tool_calls\":[{\"tool_call_id\":\"call-1\",\"arguments\":\"d\"}]}\n\n" ++
"data: {\"message_type\":\"stop_reason\",\"stop_reason\":\"requires_approval\"}\n\n";
var emitted = std.ArrayList([]u8).empty;
defer {
for (emitted.items) |chunk| std.testing.allocator.free(chunk);
emitted.deinit(std.testing.allocator);
}
const cb = struct {
fn append(ctx: ?*anyopaque, raw_chunk: []const u8) !void {
const list: *std.ArrayList([]u8) = @ptrCast(@alignCast(ctx.?));
try list.append(std.testing.allocator, try std.testing.allocator.dupe(u8, raw_chunk));
}
}.append;
const result = try parseSseAndEmit(std.testing.allocator, sse, cb, &emitted);
try std.testing.expectEqual(@as(usize, 4), emitted.items.len);
switch (result) {
.requires_approval => |calls| {
defer freePendingToolCalls(std.testing.allocator, calls);
try std.testing.expectEqual(@as(usize, 1), calls.len);
try std.testing.expectEqualStrings("call-1", calls[0].tool_call_id);
try std.testing.expectEqualStrings("Bash", calls[0].name);
try std.testing.expectEqualStrings("pwd", calls[0].arguments);
try std.testing.expectEqualStrings("msg-1", calls[0].message_id.?);
},
else => return error.ExpectedApproval,
}
}
test "incremental SSE parser emits complete CRLF event before stream end" {
const Context = struct {
count: usize = 0,
first: ?[]u8 = null,
};
var context = Context{};
defer if (context.first) |first| std.testing.allocator.free(first);
const cb = struct {
fn capture(ctx: ?*anyopaque, raw_chunk: []const u8) !void {
const capture_context: *Context = @ptrCast(@alignCast(ctx.?));
capture_context.count += 1;
if (capture_context.first == null) capture_context.first = try std.testing.allocator.dupe(u8, raw_chunk);
}
}.capture;
var parser = SseParser.init(std.testing.allocator, cb, &context);
defer parser.deinit();
try parser.feed("data: {\"message_type\":\"assistant_message\",");
try std.testing.expectEqual(@as(usize, 0), context.count);
try parser.feed("\r\ndata: \"content\":\"first\"}\r");
try std.testing.expectEqual(@as(usize, 0), context.count);
try parser.feed("\n\r\n");
try std.testing.expectEqual(@as(usize, 1), context.count);
try std.testing.expectEqualStrings("{\"message_type\":\"assistant_message\",\n\"content\":\"first\"}", context.first.?);
try parser.feed("data:{\"message_type\":\"assistant_message\",\"content\":\"later\"}\n\n");
try std.testing.expectEqual(@as(usize, 2), context.count);
const result = try parser.finish();
try std.testing.expect(result == .completed);
}
test "incremental SSE parser bounds an unfinished event" {
const cb = struct {
fn reject(_: ?*anyopaque, _: []const u8) !void {
return error.UnexpectedEmission;
}
}.reject;
var parser = SseParser.init(std.testing.allocator, cb, null);
defer parser.deinit();
const payload_bytes = 64 * 1024;
const line = try std.testing.allocator.alloc(u8, payload_bytes + "data: \n".len);
defer std.testing.allocator.free(line);
@memcpy(line[0.."data: ".len], "data: ");
@memset(line["data: ".len .. line.len - 1], 'x');
line[line.len - 1] = '\n';
const accepted_lines = (max_sse_event_bytes + 1) / (payload_bytes + 1);
for (0..accepted_lines) |_| try parser.feed(line);
try std.testing.expect(parser.event_data.items.len <= max_sse_event_bytes);
try std.testing.expectError(error.SseEventTooLarge, parser.feed(line));
try std.testing.expect(parser.event_data.items.len <= max_sse_event_bytes);
}