Skip to content

Commit 753f4c3

Browse files
committed
feat(executor): add idea selection and planning execution
Add new IdeaSelection struct to hold idea index and AI's reasoning. Implement two new executor methods: 1. runIdeaSelection() - Runs AI to select best idea from multiple options - Parses SELECTED_IDEA and REASON from response - Handles parsing errors gracefully - Returns IdeaSelection with 0-indexed idea number 2. runIdeaPlanning() - Executes planning phase for specific idea - Uses idea content and filename in prompt - Follows same retry logic as regular planning - Returns ExecutionResult (success/failure) Signed-off-by: leocavalcante <[email protected]>
1 parent 7969a72 commit 753f4c3

File tree

1 file changed

+77
-0
lines changed

1 file changed

+77
-0
lines changed

src/executor.zig

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ pub const ExecutionResult = enum {
1616
failure,
1717
};
1818

19+
/// Result of idea selection containing index and reason
20+
pub const IdeaSelection = struct {
21+
index: usize,
22+
reason: []const u8,
23+
};
24+
1925
/// Executor for running opencode CLI commands
2026
pub const Executor = struct {
2127
cfg: *const config.Config,
@@ -133,6 +139,77 @@ pub const Executor = struct {
133139
return "NEEDS_WORK";
134140
}
135141

142+
/// Run idea selection - AI picks simplest idea considering dependencies
143+
/// Returns IdeaSelection with 0-indexed idea number and reason, or null if parsing fails
144+
pub fn runIdeaSelection(self: *Executor, ideas_formatted: []const u8, cycle: u32) !?IdeaSelection {
145+
self.log.statusFmt("[Cycle {d}] Selecting idea...", .{cycle});
146+
147+
const prompt = try plan.generateIdeaSelectionPrompt(ideas_formatted, self.allocator);
148+
defer self.allocator.free(prompt);
149+
150+
var title_buf: [64]u8 = undefined;
151+
const title = std.fmt.bufPrint(&title_buf, "Opencoder Idea Selection Cycle {d}", .{cycle}) catch "Opencoder Idea Selection";
152+
153+
// Run and capture output
154+
const output = self.runOpencode(self.cfg.planning_model, title, prompt) catch |err| {
155+
self.log.logErrorFmt("[Cycle {d}] Idea selection failed: {s}", .{ cycle, @errorName(err) });
156+
return null;
157+
};
158+
defer self.allocator.free(output);
159+
160+
// Parse "SELECTED_IDEA: <number>" from output
161+
var selected_index: ?usize = null;
162+
if (std.mem.indexOf(u8, output, "SELECTED_IDEA:")) |start| {
163+
const after_colon = output[start + 14 ..]; // Skip "SELECTED_IDEA:"
164+
const trimmed = std.mem.trim(u8, after_colon, " \t\n\r");
165+
166+
// Parse the first number found
167+
var end: usize = 0;
168+
while (end < trimmed.len and trimmed[end] >= '0' and trimmed[end] <= '9') : (end += 1) {}
169+
170+
if (end > 0) {
171+
const num = std.fmt.parseInt(usize, trimmed[0..end], 10) catch return null;
172+
if (num >= 1) {
173+
selected_index = num - 1; // Convert to 0-indexed
174+
}
175+
}
176+
}
177+
178+
if (selected_index == null) {
179+
self.log.logError("Failed to parse SELECTED_IDEA from AI response");
180+
return null;
181+
}
182+
183+
// Parse "REASON: <text>" from output
184+
var reason: []const u8 = "No reason provided";
185+
if (std.mem.indexOf(u8, output, "REASON:")) |start| {
186+
const after_colon = output[start + 7 ..]; // Skip "REASON:"
187+
const trimmed = std.mem.trim(u8, after_colon, " \t");
188+
189+
// Find end of line or end of string
190+
const end = std.mem.indexOf(u8, trimmed, "\n") orelse trimmed.len;
191+
reason = std.mem.trim(u8, trimmed[0..end], " \t\n\r");
192+
}
193+
194+
return IdeaSelection{
195+
.index = selected_index.?,
196+
.reason = try self.allocator.dupe(u8, reason),
197+
};
198+
}
199+
200+
/// Run planning phase for a specific idea
201+
pub fn runIdeaPlanning(self: *Executor, idea_content: []const u8, idea_filename: []const u8, cycle: u32) !ExecutionResult {
202+
self.log.statusFmt("[Cycle {d}] Planning for idea: {s}", .{ cycle, idea_filename });
203+
204+
const prompt = try plan.generateIdeaPlanningPrompt(cycle, idea_content, idea_filename, self.allocator);
205+
defer self.allocator.free(prompt);
206+
207+
var title_buf: [64]u8 = undefined;
208+
const title = std.fmt.bufPrint(&title_buf, "Opencoder Planning Cycle {d}", .{cycle}) catch "Opencoder Planning";
209+
210+
return try self.runWithRetry(self.cfg.planning_model, title, prompt);
211+
}
212+
136213
/// Kill current child process if running
137214
pub fn killCurrentChild(self: *Executor) void {
138215
if (self.current_child_pid) |pid| {

0 commit comments

Comments
 (0)