summaryrefslogtreecommitdiffstats
path: root/src/goal.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/goal.cpp')
-rw-r--r--src/goal.cpp241
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");
+}