diff options
| author | Ethan Werner <mail@ethanwerner.com> | 2026-10-10 20:55:33 +0000 |
|---|---|---|
| committer | Ethan Werner <mail@ethanwerner.com> | 2026-10-10 20:56:50 +0000 |
| commit | f8423ebea413605b42de23f5dd06c5479d3fa1dd (patch) | |
| tree | 2612a5eb026c59e7aae01f10a1cf62f2ee43042c /src/goal.cpp | |
Diffstat (limited to 'src/goal.cpp')
| -rw-r--r-- | src/goal.cpp | 241 |
1 files changed, 241 insertions, 0 deletions
diff --git a/src/goal.cpp b/src/goal.cpp new file mode 100644 index 0000000..36aa271 --- /dev/null +++ b/src/goal.cpp @@ -0,0 +1,241 @@ +#include "goal.h" + +#include <ctype.h> +#include <stdlib.h> +#include <string.h> + +static const char *const GOAL_PREAMBLE = + "You are the reviewer for a terminal coding agent that is working toward a " + "goal. You are given the goal — a success condition — and the agent's most " + "recent reply. Decide whether the goal is fully met.\n" + "\n" + "Reply with exactly two lines:\n" + "1. Either `MET` or `NOT MET` — your verdict.\n" + "2. One short sentence: if met, what was accomplished; if not, the single " + "most important gap to fix next.\n" + "\n" + "Be strict: the goal is met only when the stated success condition is " + "clearly satisfied — not merely started, in progress, or partially done. " + "Do not give the agent the benefit of the doubt, and do not add commentary " + "beyond the two lines."; + +void goal_spec_free(goal_spec *s) +{ + if (!s) + return; + free(s->criteria); + s->criteria = NULL; +} + +void goal_report_free(goal_report *r) +{ + if (!r) + return; + free(r->criteria); + free(r); +} + +const char *goal_evaluator_preamble(void) +{ + return GOAL_PREAMBLE; +} + +static bool str_eq_i(const char *s, size_t n, const char *word) +{ + size_t wn = strlen(word); + if (n != wn) + return false; + for (size_t i = 0; i < wn; i++) { + char a = s[i], b = word[i]; + if (a >= 'a' && a <= 'z') + a = (char)(a - 'a' + 'A'); + if (b >= 'a' && b <= 'z') + b = (char)(b - 'a' + 'A'); + if (a != b) + return false; + } + return true; +} + +static int parse_tries(const char *s, size_t n, unsigned long *out, char **err) +{ + size_t tlen; + const char *t = xtrim_span(s, n, &tlen); + size_t digits = 0; + while (digits < tlen && t[digits] >= '0' && t[digits] <= '9') + digits++; + if (digits == 0) { + *err = xstrdup( + "expected a number of tries after `stop after`, e.g. " + "`stop after 5 tries`"); + return 0; + } + /* After the digits: end of input, or the word `tries` as a whole word. + * "5xyz" is rejected; "5 tries" is the canonical form. */ + size_t rest = digits; + while (rest < tlen && isspace((unsigned char)t[rest])) + rest++; + if (rest != tlen) { + size_t wn = 5; + bool word_ok = (tlen - rest) >= wn; + if (word_ok) { + const char *w = "tries"; + for (size_t i = 0; i < wn; i++) { + char a = t[rest + i], b = w[i]; + if (a >= 'a' && a <= 'z') + a = (char)(a - 'a' + 'A'); + if (b >= 'a' && b <= 'z') + b = (char)(b - 'a' + 'A'); + if (a != b) { + word_ok = false; + break; + } + } + } + if (!word_ok || ((tlen - rest) > wn && + isalpha((unsigned char)t[rest + wn]))) { + *err = xstrdup( + "expected a number of tries after `stop after`, e.g. " + "`stop after 5 tries`"); + return 0; + } + } + unsigned long value = 0; + for (size_t i = 0; i < digits; i++) { + value = value * 10UL + (unsigned long)(t[i] - '0'); + if (value > 4294967295UL) { + *err = xstrdup( + "expected a number of tries after `stop after`, e.g. " + "`stop after 5 tries`"); + return 0; + } + } + if (value == 0) { + *err = xstrdup("the goal needs at least 1 try"); + return 0; + } + *out = value; + return 1; +} + +int goal_parse(const char *input, goal_spec *out, char **err) +{ + /* Zero on entry so a failed parse leaves a freeable, empty spec. */ + memset(out, 0, sizeof *out); + const char *src = input ? input : ""; + size_t slen = strlen(src); + size_t tlen; + const char *t = xtrim_span(src, slen, &tlen); + if (tlen == 0) { + *err = xstrdup( + "give the goal its success criteria, e.g. " + "`/goal all tests pass, stop after 5 tries`"); + return 0; + } + + const char marker[] = "stop after"; + size_t mlen = sizeof(marker) - 1; + /* rfind: last occurrence of marker in [t, t+tlen). */ + const char *hit = NULL; + const char *p = t; + while (tlen - (size_t)(p - t) >= mlen) { + void *m = memmem(p, tlen - (size_t)(p - t), marker, mlen); + if (!m) + break; + hit = (const char* ) m; + p = (const char *)m + 1; + } + + if (!hit) { + out->criteria = xstrndup(t, tlen); + out->max_tries = GOAL_DEFAULT_MAX_TRIES; + return 1; + } + + size_t pre_len = (size_t)(hit - t); + size_t clen; + const char *crit = xtrim_span(t, pre_len, &clen); + while (clen > 0 && crit[clen - 1] == ',') + clen--; + crit = xtrim_span(crit, clen, &clen); + if (clen == 0) { + *err = xstrdup( + "give the goal its success criteria before `stop after`, e.g. " + "`/goal all tests pass, stop after 5 tries`"); + return 0; + } + const char *tail = hit + mlen; + if (!parse_tries(tail, tlen - (size_t)(tail - t), &out->max_tries, err)) + return 0; + out->criteria = xstrndup(crit, clen); + return 1; +} + +char *goal_prompt(const goal_spec *spec, unsigned long attempt, + const char *feedback) +{ + const char *crit = spec->criteria ? spec->criteria : ""; + if (attempt <= 1) + return xstrfmt( + "Work toward this goal until it is fully met:\n\n%s\n\n" + "Judge your own progress, verify it, and keep going if it is not " + "done. When you are confident the goal is fully met, stop and say " + "what you did.", + crit); + const char *fb = feedback ? feedback : "no specific feedback was given"; + return xstrfmt( + "You are on attempt %lu of %lu toward this goal:\n\n%s\n\n" + "A reviewer judged the goal NOT yet met: %s\n\n" + "Address that gap and keep working. When you are confident the goal is " + "fully met, stop and say what you did.", + attempt, spec->max_tries, crit, fb); +} + +/* True if `needle` occurs in `hay` as a whole word (A-Z only; `hay` is + * already upper-cased). Prevents "MET" matching inside "UNMET". */ +static bool has_word(const char *hay, const char *needle) { + size_t nl = strlen(needle); + const char *p = hay; + while ((p = strstr(p, needle)) != NULL) { + bool left_ok = (p == hay) || (p[-1] < 'A' || p[-1] > 'Z'); + bool right_ok = (p[nl] < 'A' || p[nl] > 'Z'); + if (left_ok && right_ok) + return true; + p += (nl > 0 ? nl : 1); + } + return false; +} + +void goal_parse_verdict(const char *reply, goal_verdict *out) +{ + const char *src = reply ? reply : ""; + char *upper = xstrdup(src); + for (char *c = upper; *c; c++) + if (*c >= 'a' && *c <= 'z') + *c = (char)(*c - 'a' + 'A'); + out->met = has_word(upper, "MET") && !has_word(upper, "NOT MET") && + !has_word(upper, "UNMET"); + free(upper); + + const char *chosen = NULL; + size_t chosen_len = 0; + const char *p = src; + while (*p) { + const char *nl = strchr(p, '\n'); + size_t linelen = nl ? (size_t)(nl - p) : strlen(p); + size_t tlen; + const char *line = xtrim_span(p, linelen, &tlen); + if (tlen > 0 && !str_eq_i(line, tlen, "MET") && + !str_eq_i(line, tlen, "NOT MET")) { + chosen = line; + chosen_len = tlen; + break; + } + if (!nl) + break; + p = nl + 1; + } + out->reason = chosen + ? xstrndup(chosen, chosen_len) + : xstrdup("the reviewer did not give a reason"); +} |