Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 46 additions & 9 deletions src/runtime/dispatch.zig
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,22 @@ const ResolvedAgent = types.ResolvedAgent;

/// Dispatch an agent run to the appropriate backend.
/// Writes the agent's text output to `out`.
/// If all backends fail to produce output, writes an error message to `out`
/// so the MCP caller knows the dispatch did not succeed.
pub fn dispatch(
alloc: std.mem.Allocator,
resolved: ResolvedAgent,
prompt: []const u8,
out: *std.ArrayList(u8),
) void {
const before = out.items.len;
switch (resolved.backend) {
.claude => spawnClaude(alloc, resolved, prompt, out),
.codex => spawnCodex(alloc, resolved, prompt, out),
}
if (out.items.len == before) {
out.appendSlice(alloc, "[dispatch] backend produced no output — agent run may have failed") catch {};
}
}

// ── Claude backend ────────────────────────────────────────────────────────
Expand All @@ -50,10 +56,12 @@ fn spawnClaude(
.cwd = resolved.cwd,
};

const full_prompt = if (resolved.system_prompt.len > 0)
std.fmt.allocPrint(alloc, "{s}{s}", .{ resolved.system_prompt, prompt }) catch prompt
else
prompt;
const full_prompt = if (resolved.system_prompt.len > 0) blk: {
break :blk std.fmt.allocPrint(alloc, "{s}{s}", .{ resolved.system_prompt, prompt }) catch {
std.debug.print("[dispatch] OOM: system prompt dropped for claude backend\n", .{});
break :blk prompt;
};
} else prompt;
defer if (full_prompt.ptr != prompt.ptr) alloc.free(full_prompt);

if (sdk.tryClaudeAgent(alloc, full_prompt, opts, out)) return;
Expand All @@ -74,11 +82,12 @@ fn spawnCodex(

const policy: cas.SandboxPolicy = if (resolved.writable) .writable else .read_only;

// Prepend system prompt to the user's prompt
const full_prompt = if (resolved.system_prompt.len > 0)
std.fmt.allocPrint(alloc, "{s}{s}", .{ resolved.system_prompt, prompt }) catch prompt
else
prompt;
const full_prompt = if (resolved.system_prompt.len > 0) blk: {
break :blk std.fmt.allocPrint(alloc, "{s}{s}", .{ resolved.system_prompt, prompt }) catch {
std.debug.print("[dispatch] OOM: system prompt dropped for codex backend\n", .{});
break :blk prompt;
};
} else prompt;
defer if (full_prompt.ptr != prompt.ptr) alloc.free(full_prompt);

cas.runTurnPolicy(alloc, full_prompt, out, policy);
Expand All @@ -93,3 +102,31 @@ test "dispatch: Backend.label is correct" {
try std.testing.expectEqualStrings("claude", Backend.claude.label());
try std.testing.expectEqualStrings("codex", Backend.codex.label());
}

test "dispatch: empty output sentinel is written when backend produces nothing" {
const sentinel = "[dispatch] backend produced no output — agent run may have failed";
var out: std.ArrayList(u8) = .empty;
const alloc = std.testing.allocator;
defer out.deinit(alloc);

const before = out.items.len;
// Simulate the check that dispatch() does after a backend call:
if (out.items.len == before) {
out.appendSlice(alloc, sentinel) catch {};
}
try std.testing.expectEqualStrings(sentinel, out.items);
}

test "dispatch: non-empty output does not get sentinel appended" {
const alloc = std.testing.allocator;
var out: std.ArrayList(u8) = .empty;
defer out.deinit(alloc);

try out.appendSlice(alloc, "agent reply");
const before = out.items.len;
// Simulate: backend already wrote output
if (out.items.len == before) {
out.appendSlice(alloc, "SHOULD NOT APPEAR") catch {};
}
try std.testing.expectEqualStrings("agent reply", out.items);
}
Loading