commit - 10b6dbfa3cb419e92d575e24b5f37c0748f97a97
commit + 1ea23e34d9f698ee5ed967301a83ed578127e7a4
blob - 2d9b5159b468ad7de6156909a7d880704f13c4ca
blob + 40d97bbcfbdff451c686c1743d770ecd648b8a54
--- regress/Makefile
+++ regress/Makefile
-SUBDIR= agentcfg anthropic buf conf deploy diff editor http imsgev journal json log markdown \
+SUBDIR= agentcfg anthropic buf conf deploy diff editor generation http imsgev journal json log markdown \
model_window openai output print sandbox skills sse term tools turn web xmalloc
.include <bsd.subdir.mk>
blob - 5d18e65afa8d09013140fc89fe12daa92eeb6006
blob + ab6275d337f12bc5508e5c79157d134b29749fbd
--- regress/README
+++ regress/README
input; forbidden syscalls trip
PLDG violations)
I10 mutual distrust across roles imsgev (partially consumed payloads
- fail closed); imsg length/type
+ and checked-send failures fail closed);
+ generation (canonical event ordering,
+ bounds, and exactly one terminal);
+ imsg length/type
validation in worker.c/coord.c; hub-and-spoke
authority map in proto.h; turn
(a hostile second terminal cannot
forced executor death
I13 length-aware strings buf, json (NUL round-trip), imsgev
(empty and embedded-NUL payload views),
+ generation (binary text and copied
+ canonical event ownership),
sse (NUL transparency),
anthropic, openai, http, web, output
(NUL-bearing framing, headers, bodies,
src/common/buf.c buf
src/common/json.c (+ jsmn) json
src/common/model_window.c model_window; turn (picker propagation)
+src/common/generation.c generation
src/common/sse.c sse
src/common/http.c http (hermetic TLS stub, strict chunk
data delimiters and bounded trailers)
src/fugu/priv.c, coord.c sandbox (spawn/handshake/teardown),
turn (lead and subagent turn loops)
src/fugu/journal.c journal, turn
-src/common/imsgev.c imsgev (whole-payload views); sandbox
- (event runtime)
+src/common/imsgev.c imsgev (whole-payload views, checked
+ normal/teardown sends, event failures);
+ sandbox (event runtime)
src/common/worker.c sandbox (worker runtime)
src/fugu-api (request/stream) turn (correlated concurrent lead/agent
requests), print (deadline/retry/long-key
blob - /dev/null
blob + 712cbd36a56e7eaccddbd8505a621d63585eecec (mode 644)
--- /dev/null
+++ regress/generation/Makefile
+PROG= generation_test
+SRCS= generation_test.c generation.c msg.c json.c buf.c log.c xmalloc.c
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + d54ec5cd03dff275f96e5e7eb5facf99b6a53e1b (mode 644)
--- /dev/null
+++ regress/generation/generation_test.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/queue.h>
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "msg.h"
+#include "generation.h"
+
+#include "regress.h"
+
+static void
+text_is(struct msg *m, const void *want, size_t len)
+{
+ struct block *b;
+
+ CHECK(m != NULL && m->role == ROLE_ASSISTANT);
+ b = m == NULL ? NULL : TAILQ_FIRST(&m->blocks);
+ CHECK(b != NULL && b->type == BLOCK_TEXT && b->textlen == len &&
+ memcmp(b->text, want, len) == 0);
+ CHECK(b != NULL && TAILQ_NEXT(b, entry) == NULL);
+}
+
+static int
+event_is_empty(const struct generation_event *ev)
+{
+ return (ev->kind == GENERATION_TEXT && ev->index == 0 &&
+ ev->payload.data == NULL &&
+ ev->payload.len == 0 && ev->tool_id.data == NULL &&
+ ev->tool_id.len == 0 && ev->tool_name.data == NULL &&
+ ev->tool_name.len == 0 && ev->usage.input_tokens == 0 &&
+ ev->usage.output_tokens == 0 && ev->usage.cache_read == 0 &&
+ ev->usage.cache_write == 0);
+}
+
+static size_t
+encode_begin(u_char *payload, int index, const char *id, const char *name)
+{
+ size_t idlen = strlen(id), namelen = strlen(name);
+
+ memcpy(payload, &index, sizeof(index));
+ memcpy(payload + sizeof(index), id, idlen + 1);
+ memcpy(payload + sizeof(index) + idlen + 1, name, namelen);
+ return (sizeof(index) + idlen + 1 + namelen);
+}
+
+static enum generation_feed_status
+feed_begin(struct generation *g, int index, const char *id, const char *name)
+{
+ u_char payload[sizeof(index) + GENERATION_TOOL_ID_MAX +
+ GENERATION_TOOL_NAME_MAX];
+ size_t len;
+
+ len = encode_begin(payload, index, id, name);
+ return (generation_feed(g, GENERATION_TOOL_BEGIN, payload, len, NULL));
+}
+
+static enum generation_feed_status
+feed_input(struct generation *g, int index, const void *fragment, size_t len,
+ struct generation_event *ev)
+{
+ u_char *payload;
+ enum generation_feed_status status;
+
+ payload = malloc(sizeof(index) + len);
+ if (payload == NULL)
+ abort();
+ memcpy(payload, &index, sizeof(index));
+ if (len > 0)
+ memcpy(payload + sizeof(index), fragment, len);
+ status = generation_feed(g, GENERATION_TOOL_INPUT, payload,
+ sizeof(index) + len, ev);
+ free(payload);
+ return (status);
+}
+
+static void
+test_fragmented_binary_text(void)
+{
+ struct generation *g;
+ struct generation_event ev;
+ struct generation_result r = {0};
+ char a[] = { 'a', '\0' };
+ char b[] = { 'b', 'c' };
+ static const char want[] = { 'a', '\0', 'b', 'c' };
+
+ g = generation_new(sizeof(want));
+ CHECK(generation_feed(g, GENERATION_TEXT, a, sizeof(a), &ev) ==
+ GENERATION_MORE);
+ CHECK(ev.kind == GENERATION_TEXT && ev.payload.data == a &&
+ ev.payload.len == sizeof(a));
+ CHECK(generation_feed(g, GENERATION_TEXT, b, sizeof(b), &ev) ==
+ GENERATION_MORE);
+ memset(a, 'x', sizeof(a));
+ memset(b, 'y', sizeof(b));
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, &ev) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL && r.data_used == sizeof(want) &&
+ r.tool_calls == 0 && memcmp(r.reason, "stop", 5) == 0);
+ generation_free(g);
+ text_is(r.assistant, want, sizeof(want));
+ msg_free(r.assistant);
+}
+
+static void
+test_interleaved_calls_follow_begin_order(void)
+{
+ struct generation *g;
+ struct generation_event ev;
+ struct generation_result r = {0};
+ struct block *b;
+ u_char begin[sizeof(int) + GENERATION_TOOL_ID_MAX +
+ GENERATION_TOOL_NAME_MAX];
+ u_char input[sizeof(int) + 5];
+ size_t beginlen;
+ int index;
+ static const char first[] = "{\"x\":2}";
+ static const char second[] = "{\"a\":1}";
+
+ g = generation_new(3 + sizeof(first) - 1 + sizeof(second) - 1);
+ CHECK(generation_feed(g, GENERATION_TEXT, "pre", 3, &ev) ==
+ GENERATION_MORE);
+ beginlen = encode_begin(begin, 9, "call-b", "grep");
+ CHECK(generation_feed(g, GENERATION_TOOL_BEGIN, begin, beginlen, &ev) ==
+ GENERATION_MORE);
+ CHECK(ev.index == 9 && ev.tool_id.len == 6 &&
+ memcmp(ev.tool_id.data, "call-b", 6) == 0 && ev.tool_name.len == 4 &&
+ memcmp(ev.tool_name.data, "grep", 4) == 0);
+ CHECK(feed_begin(g, 3, "call-a", "read") == GENERATION_MORE);
+ CHECK(feed_input(g, 3, "{\"a\":", 5, &ev) == GENERATION_MORE);
+ index = 9;
+ memcpy(input, &index, sizeof(index));
+ memcpy(input + sizeof(index), "{\"x\":", 5);
+ CHECK(generation_feed(g, GENERATION_TOOL_INPUT, input, sizeof(input),
+ &ev) == GENERATION_MORE);
+ CHECK(ev.index == 9 && ev.payload.len == 5 &&
+ memcmp(ev.payload.data, "{\"x\":", 5) == 0);
+ CHECK(feed_input(g, 9, "2}", 2, &ev) == GENERATION_MORE);
+ CHECK(feed_input(g, 3, "1}", 2, &ev) == GENERATION_MORE);
+ /* A normal-looking stop cannot hide tool calls; presence is canonical. */
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, &ev) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL && r.tool_calls == 2 &&
+ r.data_used == 17);
+ b = r.assistant == NULL ? NULL : TAILQ_FIRST(&r.assistant->blocks);
+ CHECK(b != NULL && b->type == BLOCK_TEXT && b->textlen == 3 &&
+ memcmp(b->text, "pre", 3) == 0);
+ b = b == NULL ? NULL : TAILQ_NEXT(b, entry);
+ CHECK(b != NULL && b->type == BLOCK_TOOL_USE &&
+ strcmp(b->tool_id, "call-b") == 0 && strcmp(b->tool_name, "grep") == 0 &&
+ b->tool_input_len == sizeof(first) - 1 &&
+ memcmp(b->tool_input, first, sizeof(first) - 1) == 0);
+ b = b == NULL ? NULL : TAILQ_NEXT(b, entry);
+ CHECK(b != NULL && b->type == BLOCK_TOOL_USE &&
+ strcmp(b->tool_id, "call-a") == 0 && strcmp(b->tool_name, "read") == 0 &&
+ b->tool_input_len == sizeof(second) - 1 &&
+ memcmp(b->tool_input, second, sizeof(second) - 1) == 0);
+ CHECK(b != NULL && TAILQ_NEXT(b, entry) == NULL);
+ if (r.assistant != NULL)
+ msg_free(r.assistant);
+ generation_free(g);
+}
+
+static void
+test_noargs_uses_exact_budget(void)
+{
+ struct generation *g;
+ struct generation_result r = {0};
+ struct block *b;
+
+ g = generation_new(2);
+ CHECK(feed_begin(g, 0, "none", "ls") == GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "tool_use", 8, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL && r.data_used == 2 &&
+ r.tool_calls == 1);
+ b = r.assistant == NULL ? NULL : TAILQ_FIRST(&r.assistant->blocks);
+ CHECK(b != NULL && b->type == BLOCK_TOOL_USE &&
+ b->tool_input_len == 2 && memcmp(b->tool_input, "{}", 2) == 0);
+ if (r.assistant != NULL)
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(1);
+ CHECK(feed_begin(g, 0, "none", "ls") == GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "tool_use", 8, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ r.data_used == 0 &&
+ strcmp(r.reason, "Generation data bound exceeded") == 0);
+ generation_free(g);
+}
+
+static void
+test_malformed_arguments_are_never_accepted(void)
+{
+ static const struct {
+ const void *data;
+ size_t len;
+ } bad[] = {
+ { "[]", 2 },
+ { "null", 4 },
+ { "{\"a\":}", 6 },
+ { "{\"a\":1} trailing", 16 },
+ { "{\"a\":\"x\0y\"}", 11 }
+ };
+ struct generation *g;
+ struct generation_result r;
+ size_t i;
+
+ for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
+ memset(&r, 0, sizeof(r));
+ g = generation_new(bad[i].len);
+ CHECK(feed_begin(g, 0, "bad", "read") ==
+ GENERATION_MORE);
+ CHECK(feed_input(g, 0, bad[i].data, bad[i].len, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE,
+ i == 0 ? "max_tokens" : "tool_use",
+ i == 0 ? 10 : 8, NULL) == GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ strcmp(r.reason,
+ "provider sent malformed tool arguments") == 0);
+ generation_free(g);
+ }
+}
+
+static void
+test_length_outcome_and_empty_placeholder(void)
+{
+ static const char placeholder[] = GENERATION_LENGTH_EMPTY;
+ struct generation *g;
+ struct generation_result r = {0};
+ struct block *b;
+
+ g = generation_new(sizeof(placeholder) - 1);
+ CHECK(generation_feed(g, GENERATION_DONE, "max_tokens", 10, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_LENGTH &&
+ r.data_used == sizeof(placeholder) - 1);
+ text_is(r.assistant, placeholder, sizeof(placeholder) - 1);
+ if (r.assistant != NULL)
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(sizeof(placeholder) - 2);
+ CHECK(generation_feed(g, GENERATION_DONE, "length", 6, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ strcmp(r.reason, "Generation data bound exceeded") == 0);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "length", 6, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_LENGTH && r.data_used == 1);
+ text_is(r.assistant, "x", 1);
+ if (r.assistant != NULL)
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(0);
+ CHECK(generation_feed(g, GENERATION_DONE, "tool_use", 8, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ strcmp(r.reason, "provider sent an empty reply") == 0);
+ generation_free(g);
+
+ /* Conversely, a tool-looking opaque stop creates no call by itself. */
+ memset(&r, 0, sizeof(r));
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "tool_use", 8, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL && r.tool_calls == 0);
+ msg_free(r.assistant);
+ generation_free(g);
+
+ /* A capped call remains canonical data; Adapter policy decides execution. */
+ memset(&r, 0, sizeof(r));
+ g = generation_new(2);
+ CHECK(feed_begin(g, 0, "capped", "read") == GENERATION_MORE);
+ CHECK(feed_input(g, 0, "{}", 2, NULL) == GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "max_tokens", 10, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_LENGTH && r.tool_calls == 1 &&
+ r.data_used == 2);
+ b = r.assistant == NULL ? NULL : TAILQ_FIRST(&r.assistant->blocks);
+ CHECK(b != NULL && b->type == BLOCK_TOOL_USE &&
+ strcmp(b->tool_id, "capped") == 0 &&
+ b->tool_input_len == 2 && memcmp(b->tool_input, "{}", 2) == 0);
+ msg_free(r.assistant);
+ generation_free(g);
+}
+
+static void
+test_usage_retry_and_provider_error(void)
+{
+ struct generation *g;
+ struct generation_event ev;
+ struct generation_result r = {0};
+ struct generation_usage u1 = { 1, 2, 3, 4 };
+ struct generation_usage u2 = { 10, 20, 30, 40 };
+ char error[] = "provider broke";
+
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_USAGE, &u1, sizeof(u1), &ev) ==
+ GENERATION_MORE);
+ CHECK(ev.usage.input_tokens == 1 && ev.usage.output_tokens == 2 &&
+ ev.usage.cache_read == 3 && ev.usage.cache_write == 4);
+ CHECK(generation_feed(g, GENERATION_USAGE, &u2, sizeof(u2), NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_RETRY, "retrying", 8, &ev) ==
+ GENERATION_MORE);
+ CHECK(ev.payload.len == 8 &&
+ memcmp(ev.payload.data, "retrying", 8) == 0);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.usage.input_tokens == 11 && r.usage.output_tokens == 22 &&
+ r.usage.cache_read == 33 && r.usage.cache_write == 44);
+ if (r.assistant != NULL)
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(3);
+ CHECK(generation_feed(g, GENERATION_TEXT, "abc", 3, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_ERROR, error,
+ sizeof(error) - 1, &ev) == GENERATION_TERMINAL);
+ CHECK(ev.payload.len == sizeof(error) - 1 && ev.payload.data == error);
+ memset(error, 'x', sizeof(error) - 1);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_PROVIDER_ERROR && r.assistant == NULL &&
+ r.data_used == 3 && strcmp(r.reason, "provider broke") == 0);
+ generation_free(g);
+}
+
+static void
+test_usage_overflow_drains_to_terminal(void)
+{
+ struct generation *g;
+ struct generation_event ev;
+ struct generation_result r = {0};
+ struct generation_usage u = { GENERATION_USAGE_EVENT_MAX, 0, 0, 0 };
+ struct generation_usage seven = { 7, 0, 0, 0 };
+ struct generation_usage one = { 1, 0, 0, 0 };
+ int i;
+
+ /* The per-event cap is not an aggregate cap. */
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_USAGE, &u, sizeof(u), NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_USAGE, &one, sizeof(one), NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL && r.assistant != NULL &&
+ r.usage.input_tokens == GENERATION_USAGE_EVENT_MAX + 1);
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(10);
+ for (i = 0; i < 100; i++)
+ CHECK(generation_feed(g, GENERATION_USAGE, &u, sizeof(u),
+ NULL) == GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_USAGE, &seven, sizeof(seven),
+ NULL) == GENERATION_MORE);
+ memset(&ev, 0xa5, sizeof(ev));
+ CHECK(generation_feed(g, GENERATION_USAGE, &one, sizeof(one), &ev) ==
+ GENERATION_DRAIN);
+ CHECK(event_is_empty(&ev));
+ CHECK(generation_feed(g, GENERATION_TEXT, "ignored", 7, NULL) ==
+ GENERATION_DRAIN);
+ CHECK(generation_feed(g, GENERATION_ERROR, "later provider error", 20,
+ NULL) == GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ r.usage.input_tokens == INT64_MAX &&
+ strcmp(r.reason, "Generation usage bound exceeded") == 0);
+ generation_free(g);
+}
+
+static void
+test_usage_values_are_bounded(void)
+{
+ struct generation *g;
+ struct generation_result r = {0};
+ struct generation_usage bad[] = {
+ { -1, 0, 0, 0 },
+ { 0, GENERATION_USAGE_EVENT_MAX + 1, 0, 0 }
+ };
+ size_t i;
+
+ for (i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
+ g = generation_new(0);
+ CHECK(generation_feed(g, GENERATION_USAGE, &bad[i],
+ sizeof(bad[i]), NULL) == GENERATION_DRAIN);
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ strcmp(r.reason, "Generation usage bound exceeded") == 0);
+ generation_free(g);
+ }
+}
+
+static void
+test_data_budget_drains_to_terminal(void)
+{
+ struct generation *g;
+ struct generation_event ev;
+ struct generation_result r = {0};
+ u_char input[sizeof(int) + 1];
+ int index = 0;
+
+ g = generation_new(3);
+ CHECK(generation_feed(g, GENERATION_TEXT, "abc", 3, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL && r.data_used == 3);
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(3);
+ CHECK(generation_feed(g, GENERATION_TEXT, "ab", 2, NULL) ==
+ GENERATION_MORE);
+ memset(&ev, 0xa5, sizeof(ev));
+ CHECK(generation_feed(g, GENERATION_TEXT, "cd", 2, &ev) ==
+ GENERATION_DRAIN);
+ CHECK(event_is_empty(&ev));
+ memset(&ev, 0xa5, sizeof(ev));
+ CHECK(generation_feed(g, GENERATION_TOOL_INPUT, NULL, 0, &ev) ==
+ GENERATION_DRAIN);
+ CHECK(event_is_empty(&ev));
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ r.data_used == 2 &&
+ strcmp(r.reason, "Generation data bound exceeded") == 0);
+ generation_free(g);
+
+ g = generation_new(1);
+ CHECK(feed_begin(g, index, "call", "read") == GENERATION_MORE);
+ memcpy(input, &index, sizeof(index));
+ input[sizeof(index)] = '{';
+ CHECK(generation_feed(g, GENERATION_TOOL_INPUT, input, sizeof(input),
+ NULL) == GENERATION_MORE);
+ input[sizeof(index)] = '}';
+ memset(&ev, 0xa5, sizeof(ev));
+ CHECK(generation_feed(g, GENERATION_TOOL_INPUT, input, sizeof(input),
+ &ev) == GENERATION_DRAIN);
+ CHECK(event_is_empty(&ev));
+ generation_free(g);
+}
+
+static void
+test_call_bound_is_64(void)
+{
+ struct generation *g;
+ struct generation_event ev;
+ struct generation_result r = {0};
+ u_char begin[sizeof(int) + GENERATION_TOOL_ID_MAX +
+ GENERATION_TOOL_NAME_MAX];
+ char id[32];
+ size_t beginlen;
+ int i;
+
+ g = generation_new(GENERATION_CALL_MAX * 2);
+ for (i = 0; i < GENERATION_CALL_MAX; i++) {
+ (void)snprintf(id, sizeof(id), "call-%d", i);
+ if (feed_begin(g, i, id, "ls") != GENERATION_MORE)
+ break;
+ }
+ CHECK(i == GENERATION_CALL_MAX);
+ CHECK(generation_feed(g, GENERATION_DONE, "tool_use", 8, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL &&
+ r.tool_calls == GENERATION_CALL_MAX &&
+ r.data_used == GENERATION_CALL_MAX * 2);
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ g = generation_new(GENERATION_CALL_MAX * 2);
+ for (i = 0; i < GENERATION_CALL_MAX; i++) {
+ (void)snprintf(id, sizeof(id), "call-%d", i);
+ if (feed_begin(g, i, id, "ls") != GENERATION_MORE)
+ break;
+ }
+ CHECK(i == GENERATION_CALL_MAX);
+ beginlen = encode_begin(begin, GENERATION_CALL_MAX, "one-too-many",
+ "ls");
+ memset(&ev, 0xa5, sizeof(ev));
+ CHECK(generation_feed(g, GENERATION_TOOL_BEGIN, begin, beginlen, &ev) ==
+ GENERATION_DRAIN);
+ CHECK(event_is_empty(&ev));
+ CHECK(generation_feed(g, GENERATION_DONE, "tool_use", 8, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_INVALID && r.assistant == NULL &&
+ r.tool_calls == GENERATION_CALL_MAX && r.data_used == 0 &&
+ strcmp(r.reason, "Generation tool-call bound exceeded") == 0);
+ generation_free(g);
+}
+
+static void
+check_protocol_event(enum generation_kind kind, const void *data, size_t len)
+{
+ struct generation *g;
+
+ g = generation_new(1024);
+ CHECK(generation_feed(g, kind, data, len, NULL) == GENERATION_PROTOCOL);
+ CHECK(generation_error(g) != NULL);
+ generation_free(g);
+}
+
+static void
+test_malformed_payloads_and_order_are_protocol_errors(void)
+{
+ enum { RETRY_WIRE_MAX = 1200 };
+ u_char short_begin[sizeof(int) + 2] = {0};
+ u_char no_nul[sizeof(int) + 3];
+ u_char empty_id[sizeof(int) + 2];
+ u_char long_id[sizeof(int) + GENERATION_TOOL_ID_MAX + 2];
+ u_char empty_name[sizeof(int) + 2];
+ u_char long_name[sizeof(int) + 2 + GENERATION_TOOL_NAME_MAX];
+ u_char nul_name[sizeof(int) + 5];
+ u_char short_input[sizeof(int) - 1] = {0};
+ char oversized[GENERATION_ERROR_MAX];
+ char retry_ok[RETRY_WIRE_MAX - 1];
+ char retry_too_long[RETRY_WIRE_MAX];
+ struct generation_usage usage = {0};
+ struct generation *g;
+ struct generation_event ev;
+ int index = 0;
+
+ check_protocol_event(GENERATION_TEXT, NULL, 1);
+ check_protocol_event(GENERATION_TOOL_BEGIN, short_begin,
+ sizeof(short_begin));
+
+ memcpy(no_nul, &index, sizeof(index));
+ memcpy(no_nul + sizeof(index), "abc", 3);
+ check_protocol_event(GENERATION_TOOL_BEGIN, no_nul, sizeof(no_nul));
+
+ memcpy(empty_id, &index, sizeof(index));
+ empty_id[sizeof(index)] = '\0';
+ empty_id[sizeof(index) + 1] = 'x';
+ check_protocol_event(GENERATION_TOOL_BEGIN, empty_id, sizeof(empty_id));
+
+ memcpy(long_id, &index, sizeof(index));
+ memset(long_id + sizeof(index), 'i', GENERATION_TOOL_ID_MAX);
+ long_id[sizeof(index) + GENERATION_TOOL_ID_MAX] = '\0';
+ long_id[sizeof(index) + GENERATION_TOOL_ID_MAX + 1] = 'x';
+ check_protocol_event(GENERATION_TOOL_BEGIN, long_id, sizeof(long_id));
+
+ memcpy(empty_name, &index, sizeof(index));
+ empty_name[sizeof(index)] = 'i';
+ empty_name[sizeof(index) + 1] = '\0';
+ check_protocol_event(GENERATION_TOOL_BEGIN, empty_name,
+ sizeof(empty_name));
+
+ memcpy(long_name, &index, sizeof(index));
+ long_name[sizeof(index)] = 'i';
+ long_name[sizeof(index) + 1] = '\0';
+ memset(long_name + sizeof(index) + 2, 'n', GENERATION_TOOL_NAME_MAX);
+ check_protocol_event(GENERATION_TOOL_BEGIN, long_name,
+ sizeof(long_name));
+
+ memcpy(nul_name, &index, sizeof(index));
+ memcpy(nul_name + sizeof(index), "i\0a\0b", 5);
+ check_protocol_event(GENERATION_TOOL_BEGIN, nul_name, sizeof(nul_name));
+
+ g = generation_new(10);
+ CHECK(feed_begin(g, -1, "id", "ls") == GENERATION_PROTOCOL);
+ generation_free(g);
+ g = generation_new(10);
+ CHECK(feed_begin(g, 0, "id", "ls") == GENERATION_MORE);
+ CHECK(feed_begin(g, 0, "other", "read") == GENERATION_PROTOCOL);
+ generation_free(g);
+ g = generation_new(10);
+ CHECK(feed_begin(g, 0, "same-id", "ls") == GENERATION_MORE);
+ CHECK(feed_begin(g, 1, "same-id", "read") == GENERATION_PROTOCOL);
+ generation_free(g);
+
+ check_protocol_event(GENERATION_TOOL_INPUT, short_input,
+ sizeof(short_input));
+ g = generation_new(10);
+ CHECK(feed_input(g, 7, "{}", 2, NULL) == GENERATION_PROTOCOL);
+ generation_free(g);
+
+ check_protocol_event(GENERATION_USAGE, &usage, sizeof(usage) - 1);
+ check_protocol_event(GENERATION_RETRY, NULL, 0);
+ check_protocol_event(GENERATION_RETRY, "a\0b", 3);
+ memset(retry_ok, 'x', sizeof(retry_ok));
+ g = generation_new(0);
+ CHECK(generation_feed(g, GENERATION_RETRY, retry_ok, sizeof(retry_ok),
+ &ev) == GENERATION_MORE);
+ CHECK(ev.payload.data == retry_ok && ev.payload.len == sizeof(retry_ok));
+ generation_free(g);
+ memset(retry_too_long, 'x', sizeof(retry_too_long));
+ check_protocol_event(GENERATION_RETRY, retry_too_long,
+ sizeof(retry_too_long));
+ memset(oversized, 'x', sizeof(oversized));
+ check_protocol_event(GENERATION_DONE, NULL, 0);
+ check_protocol_event(GENERATION_DONE, "a\0b", 3);
+ check_protocol_event(GENERATION_DONE, oversized, GENERATION_STOP_MAX);
+ check_protocol_event(GENERATION_ERROR, NULL, 0);
+ check_protocol_event(GENERATION_ERROR, "a\0b", 3);
+ check_protocol_event(GENERATION_ERROR, oversized,
+ GENERATION_ERROR_MAX);
+ check_protocol_event((enum generation_kind)999, NULL, 0);
+}
+
+static void
+test_exact_payload_maxima(void)
+{
+ struct generation *g;
+ struct generation_result r = {0};
+ char id[GENERATION_TOOL_ID_MAX];
+ char name[GENERATION_TOOL_NAME_MAX];
+ char stop[GENERATION_STOP_MAX - 1];
+ char error[GENERATION_ERROR_MAX - 1];
+
+ memset(id, 'i', sizeof(id) - 1);
+ id[sizeof(id) - 1] = '\0';
+ memset(name, 'n', sizeof(name) - 1);
+ name[sizeof(name) - 1] = '\0';
+ g = generation_new(2);
+ CHECK(feed_begin(g, 0, id, name) == GENERATION_MORE);
+ CHECK(feed_input(g, 0, "{}", 2, NULL) == GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "tool_use", 8, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL && r.tool_calls == 1);
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ memset(stop, 's', sizeof(stop));
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, stop, sizeof(stop), NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_NORMAL &&
+ memcmp(r.reason, stop, sizeof(stop)) == 0 &&
+ r.reason[sizeof(stop)] == '\0');
+ msg_free(r.assistant);
+ generation_free(g);
+
+ memset(&r, 0, sizeof(r));
+ memset(error, 'e', sizeof(error));
+ g = generation_new(0);
+ CHECK(generation_feed(g, GENERATION_ERROR, error, sizeof(error), NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(r.outcome == GENERATION_PROVIDER_ERROR && r.assistant == NULL &&
+ memcmp(r.reason, error, sizeof(error)) == 0 &&
+ r.reason[sizeof(error)] == '\0');
+ generation_free(g);
+}
+
+static void
+test_terminal_and_take_are_exactly_once(void)
+{
+ struct generation *g;
+ struct generation_result r = {0};
+
+ g = generation_new(1);
+ CHECK(generation_take(g, &r) == -1);
+ CHECK(strcmp(generation_error(g), "Generation is not terminal") == 0);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_take(g, &r) == 0);
+ CHECK(generation_take(g, &r) == -1);
+ CHECK(strcmp(generation_error(g), "Generation result already taken") == 0);
+ msg_free(r.assistant);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_PROTOCOL);
+ generation_free(g);
+
+ g = generation_new(1);
+ CHECK(generation_feed(g, GENERATION_TEXT, "x", 1, NULL) ==
+ GENERATION_MORE);
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_TERMINAL);
+ CHECK(generation_feed(g, GENERATION_DONE, "stop", 4, NULL) ==
+ GENERATION_PROTOCOL);
+ CHECK(generation_take(g, &r) == -1);
+ generation_free(g);
+}
+
+int
+main(void)
+{
+ test_fragmented_binary_text();
+ test_interleaved_calls_follow_begin_order();
+ test_noargs_uses_exact_budget();
+ test_malformed_arguments_are_never_accepted();
+ test_length_outcome_and_empty_placeholder();
+ test_usage_retry_and_provider_error();
+ test_usage_overflow_drains_to_terminal();
+ test_usage_values_are_bounded();
+ test_data_budget_drains_to_terminal();
+ test_call_bound_is_64();
+ test_malformed_payloads_and_order_are_protocol_errors();
+ test_exact_payload_maxima();
+ test_terminal_and_take_are_exactly_once();
+ REGRESS_END();
+}
blob - /dev/null
blob + e0c5c4c60c7b8e667a2abca96e8e692961c06e20 (mode 644)
--- /dev/null
+++ src/common/generation.c
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <sys/types.h>
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "xmalloc.h"
+#include "buf.h"
+#include "json.h"
+#include "msg.h"
+#include "generation.h"
+
+struct generation_call {
+ int index;
+ char id[GENERATION_TOOL_ID_MAX];
+ char name[GENERATION_TOOL_NAME_MAX];
+ struct buf input;
+};
+
+struct generation {
+ struct buf text;
+ struct generation_call calls[GENERATION_CALL_MAX];
+ size_t ncalls;
+ size_t data_available;
+ size_t data_used;
+ struct generation_usage usage;
+ struct msg *assistant;
+ enum generation_outcome outcome;
+ char reason[GENERATION_ERROR_MAX];
+ char error[GENERATION_ERROR_MAX];
+ int terminal;
+ int drain;
+ int protocol;
+ int taken;
+};
+
+static void
+generation_set_error(struct generation *g, const char *s)
+{
+ size_t len;
+
+ len = strlen(s);
+ if (len >= sizeof(g->error))
+ len = sizeof(g->error) - 1;
+ memcpy(g->error, s, len);
+ g->error[len] = '\0';
+}
+
+static enum generation_feed_status
+generation_protocol(struct generation *g, const char *s)
+{
+ g->protocol = 1;
+ generation_set_error(g, s);
+ return (GENERATION_PROTOCOL);
+}
+
+static enum generation_feed_status
+generation_drain_event(struct generation_event *ev)
+{
+ if (ev != NULL)
+ memset(ev, 0, sizeof(*ev));
+ return (GENERATION_DRAIN);
+}
+
+static struct generation_call *
+generation_call_find(struct generation *g, int index)
+{
+ size_t i;
+
+ for (i = 0; i < g->ncalls; i++)
+ if (g->calls[i].index == index)
+ return (&g->calls[i]);
+ return (NULL);
+}
+
+static int
+generation_call_id_exists(const struct generation *g, const void *id,
+ size_t len)
+{
+ size_t i;
+
+ for (i = 0; i < g->ncalls; i++)
+ if (strlen(g->calls[i].id) == len &&
+ memcmp(g->calls[i].id, id, len) == 0)
+ return (1);
+ return (0);
+}
+
+static int
+generation_account(struct generation *g, size_t len)
+{
+ if (len > g->data_available - g->data_used) {
+ g->drain = 1;
+ g->outcome = GENERATION_INVALID;
+ generation_set_error(g, "Generation data bound exceeded");
+ return (-1);
+ }
+ g->data_used += len;
+ return (0);
+}
+
+static int
+generation_usage_add(struct generation *g, const struct generation_usage *u)
+{
+ struct generation_usage sum = g->usage;
+
+#define ADD_USAGE(field) do { \
+ if (u->field < 0 || u->field > GENERATION_USAGE_EVENT_MAX || \
+ u->field > INT64_MAX - sum.field) \
+ return (-1); \
+ sum.field += u->field; \
+} while (0)
+ ADD_USAGE(input_tokens);
+ ADD_USAGE(output_tokens);
+ ADD_USAGE(cache_read);
+ ADD_USAGE(cache_write);
+#undef ADD_USAGE
+ g->usage = sum;
+ return (0);
+}
+
+static void
+generation_build_assistant(struct generation *g)
+{
+ struct generation_call *call;
+ size_t i;
+
+ g->assistant = msg_new(ROLE_ASSISTANT);
+ if (g->text.len > 0)
+ msg_add_text(g->assistant, (const char *)g->text.data, g->text.len);
+ for (i = 0; i < g->ncalls; i++) {
+ call = &g->calls[i];
+ msg_add_tool_use(g->assistant, call->id, call->name,
+ (const char *)call->input.data, call->input.len);
+ }
+}
+
+struct generation *
+generation_new(size_t data_available)
+{
+ struct generation *g;
+
+ g = xcalloc(1, sizeof(*g));
+ buf_init(&g->text);
+ g->data_available = data_available;
+ return (g);
+}
+
+enum generation_feed_status
+generation_feed(struct generation *g, enum generation_kind kind,
+ const void *data, size_t len, struct generation_event *ev)
+{
+ struct generation_call *call;
+ const u_char *p = data;
+ size_t idlen, namelen, fragmentlen, i;
+ int index, limited;
+
+ if (g == NULL)
+ return (GENERATION_PROTOCOL);
+ if (ev != NULL) {
+ memset(ev, 0, sizeof(*ev));
+ ev->kind = kind;
+ }
+ if (g->terminal || g->taken)
+ return (generation_protocol(g, "event after Generation terminal"));
+ if (g->protocol)
+ return (GENERATION_PROTOCOL);
+ if (len > 0 && data == NULL)
+ return (generation_protocol(g, "missing Generation payload"));
+ if (g->drain && kind != GENERATION_DONE && kind != GENERATION_ERROR)
+ return (generation_drain_event(ev));
+
+ switch (kind) {
+ case GENERATION_TEXT:
+ if (ev != NULL) {
+ ev->payload.data = data;
+ ev->payload.len = len;
+ }
+ if (g->drain)
+ return (generation_drain_event(ev));
+ if (generation_account(g, len) == -1)
+ return (generation_drain_event(ev));
+ if (len > 0)
+ buf_add(&g->text, data, len);
+ return (GENERATION_MORE);
+ case GENERATION_TOOL_BEGIN:
+ if (len < sizeof(index) + 3)
+ return (generation_protocol(g, "malformed tool begin"));
+ memcpy(&index, p, sizeof(index));
+ p += sizeof(index);
+ len -= sizeof(index);
+ if (index < 0 || (p = memchr(p, '\0', len)) == NULL)
+ return (generation_protocol(g, "malformed tool begin"));
+ idlen = (size_t)(p - (const u_char *)data - sizeof(index));
+ namelen = len - idlen - 1;
+ if (idlen == 0 || idlen >= GENERATION_TOOL_ID_MAX ||
+ namelen == 0 || namelen >= GENERATION_TOOL_NAME_MAX ||
+ memchr(p + 1, '\0', namelen) != NULL ||
+ generation_call_find(g, index) != NULL ||
+ generation_call_id_exists(g,
+ (const u_char *)data + sizeof(index), idlen))
+ return (generation_protocol(g, "malformed tool begin"));
+ if (ev != NULL) {
+ ev->index = index;
+ ev->tool_id.data = (const u_char *)data + sizeof(index);
+ ev->tool_id.len = idlen;
+ ev->tool_name.data = p + 1;
+ ev->tool_name.len = namelen;
+ }
+ if (g->drain)
+ return (generation_drain_event(ev));
+ if (g->ncalls >= GENERATION_CALL_MAX) {
+ g->drain = 1;
+ g->outcome = GENERATION_INVALID;
+ generation_set_error(g, "Generation tool-call bound exceeded");
+ return (generation_drain_event(ev));
+ }
+ call = &g->calls[g->ncalls++];
+ memset(call, 0, sizeof(*call));
+ call->index = index;
+ memcpy(call->id, (const u_char *)data + sizeof(index), idlen);
+ call->id[idlen] = '\0';
+ memcpy(call->name, p + 1, namelen);
+ call->name[namelen] = '\0';
+ buf_init(&call->input);
+ return (GENERATION_MORE);
+ case GENERATION_TOOL_INPUT:
+ if (len < sizeof(index))
+ return (generation_protocol(g, "malformed tool input"));
+ memcpy(&index, data, sizeof(index));
+ if (index < 0 ||
+ (call = generation_call_find(g, index)) == NULL)
+ return (generation_protocol(g, "tool input before begin"));
+ fragmentlen = len - sizeof(index);
+ if (ev != NULL) {
+ ev->index = index;
+ ev->payload.data = (const u_char *)data + sizeof(index);
+ ev->payload.len = fragmentlen;
+ }
+ if (g->drain)
+ return (generation_drain_event(ev));
+ if (generation_account(g, fragmentlen) == -1)
+ return (generation_drain_event(ev));
+ if (fragmentlen > 0)
+ buf_add(&call->input,
+ (const u_char *)data + sizeof(index), fragmentlen);
+ return (GENERATION_MORE);
+ case GENERATION_USAGE: {
+ struct generation_usage u;
+
+ if (len != sizeof(u))
+ return (generation_protocol(g, "malformed Generation usage"));
+ memcpy(&u, data, sizeof(u));
+ if (ev != NULL)
+ ev->usage = u;
+ if (generation_usage_add(g, &u) == -1) {
+ g->drain = 1;
+ g->outcome = GENERATION_INVALID;
+ generation_set_error(g,
+ "Generation usage bound exceeded");
+ return (generation_drain_event(ev));
+ }
+ return (GENERATION_MORE);
+ }
+ case GENERATION_RETRY:
+ if (len == 0 || len >= GENERATION_RETRY_MAX ||
+ memchr(data, '\0', len) != NULL)
+ return (generation_protocol(g,
+ "malformed Generation retry notice"));
+ if (ev != NULL) {
+ ev->payload.data = data;
+ ev->payload.len = len;
+ }
+ return (GENERATION_MORE);
+ case GENERATION_DONE:
+ if (len == 0 || len >= GENERATION_STOP_MAX ||
+ memchr(data, '\0', len) != NULL)
+ return (generation_protocol(g,
+ "malformed Generation stop reason"));
+ if (ev != NULL) {
+ ev->payload.data = data;
+ ev->payload.len = len;
+ }
+ memcpy(g->reason, data, len);
+ g->reason[len] = '\0';
+ g->terminal = 1;
+ limited = (len == 10 && memcmp(data, "max_tokens", 10) == 0) ||
+ (len == 6 && memcmp(data, "length", 6) == 0);
+ if (g->drain) {
+ memcpy(g->reason, g->error, strlen(g->error) + 1);
+ return (GENERATION_TERMINAL);
+ }
+ if (g->text.len == 0 && g->ncalls == 0 && limited) {
+ if (generation_account(g,
+ sizeof(GENERATION_LENGTH_EMPTY) - 1) == -1) {
+ memcpy(g->reason, g->error, strlen(g->error) + 1);
+ return (GENERATION_TERMINAL);
+ }
+ buf_add(&g->text, GENERATION_LENGTH_EMPTY,
+ sizeof(GENERATION_LENGTH_EMPTY) - 1);
+ } else if (g->text.len == 0 && g->ncalls == 0) {
+ g->outcome = GENERATION_INVALID;
+ generation_set_error(g, "provider sent an empty reply");
+ memcpy(g->reason, g->error, strlen(g->error) + 1);
+ return (GENERATION_TERMINAL);
+ }
+ for (i = 0; i < g->ncalls; i++) {
+ if (g->calls[i].input.len == 0) {
+ if (generation_account(g, 2) == -1) {
+ memcpy(g->reason, g->error,
+ strlen(g->error) + 1);
+ return (GENERATION_TERMINAL);
+ }
+ buf_addstr(&g->calls[i].input, "{}");
+ }
+ if (!json_valid_object(g->calls[i].input.data,
+ g->calls[i].input.len)) {
+ g->outcome = GENERATION_INVALID;
+ generation_set_error(g,
+ "provider sent malformed tool arguments");
+ memcpy(g->reason, g->error, strlen(g->error) + 1);
+ return (GENERATION_TERMINAL);
+ }
+ }
+ g->outcome = limited ? GENERATION_LENGTH : GENERATION_NORMAL;
+ generation_build_assistant(g);
+ return (GENERATION_TERMINAL);
+ case GENERATION_ERROR:
+ if (len == 0 || len >= GENERATION_ERROR_MAX ||
+ memchr(data, '\0', len) != NULL)
+ return (generation_protocol(g,
+ "malformed Generation provider error"));
+ if (ev != NULL) {
+ ev->payload.data = data;
+ ev->payload.len = len;
+ }
+ g->terminal = 1;
+ if (g->drain) {
+ memcpy(g->reason, g->error, strlen(g->error) + 1);
+ return (GENERATION_TERMINAL);
+ }
+ memcpy(g->reason, data, len);
+ g->reason[len] = '\0';
+ g->outcome = GENERATION_PROVIDER_ERROR;
+ generation_set_error(g, g->reason);
+ return (GENERATION_TERMINAL);
+ default:
+ return (generation_protocol(g, "unknown Generation event"));
+ }
+}
+
+int
+generation_take(struct generation *g, struct generation_result *r)
+{
+ if (g == NULL || r == NULL || !g->terminal || g->protocol || g->taken) {
+ if (g != NULL && !g->terminal && !g->protocol)
+ generation_set_error(g, "Generation is not terminal");
+ else if (g != NULL && g->taken)
+ generation_set_error(g, "Generation result already taken");
+ return (-1);
+ }
+ memset(r, 0, sizeof(*r));
+ r->outcome = g->outcome;
+ r->assistant = g->assistant;
+ r->usage = g->usage;
+ r->data_used = g->data_used;
+ r->tool_calls = g->ncalls;
+ r->reason = g->reason;
+ g->assistant = NULL;
+ g->taken = 1;
+ return (0);
+}
+
+const char *
+generation_error(const struct generation *g)
+{
+ if (g == NULL || g->error[0] == '\0')
+ return (NULL);
+ return (g->error);
+}
+
+void
+generation_free(struct generation *g)
+{
+ size_t i;
+
+ if (g == NULL)
+ return;
+ if (g->assistant != NULL)
+ msg_free(g->assistant);
+ for (i = 0; i < g->ncalls; i++)
+ buf_free(&g->calls[i].input);
+ buf_free(&g->text);
+ free(g);
+}
blob - /dev/null
blob + 2a35d2e3284e55032a69b1dc72a7d0b1673fca21 (mode 644)
--- /dev/null
+++ src/common/generation.h
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef GENERATION_H
+#define GENERATION_H
+
+#include <sys/types.h>
+
+#include <stdint.h>
+
+struct msg;
+struct generation;
+
+#define GENERATION_CALL_MAX 64
+#define GENERATION_TOOL_ID_MAX 256
+#define GENERATION_TOOL_NAME_MAX 32
+#define GENERATION_STOP_MAX 64
+#define GENERATION_RETRY_MAX 1200
+#define GENERATION_ERROR_MAX 1024
+#define GENERATION_USAGE_EVENT_MAX (INT64_MAX / 100)
+#define GENERATION_LENGTH_EMPTY "Generation stopped before emitting content."
+
+/*
+ * Canonical payload kinds after a Provider codec has decoded the wire.
+ * TEXT, RETRY, DONE, and ERROR carry their bytes directly. TOOL_BEGIN is
+ * `int index`, then `id\0name` (name is length-carried). TOOL_INPUT is
+ * `int index`, then one argument fragment. USAGE is exactly one
+ * struct generation_usage. The native in-process representation is
+ * deliberate: an Adapter, not this Module, owns any transport encoding.
+ * Tool indices and ids are each unique within one Generation.
+ */
+enum generation_kind {
+ GENERATION_TEXT,
+ GENERATION_TOOL_BEGIN,
+ GENERATION_TOOL_INPUT,
+ GENERATION_USAGE,
+ GENERATION_RETRY,
+ GENERATION_DONE,
+ GENERATION_ERROR
+};
+
+/*
+ * One feed either extends the Generation, ends it, or enters drain mode.
+ * PROTOCOL makes the Generation unusable. DRAIN latches a local bound;
+ * callers continue feeding until the first valid DONE or ERROR, whose
+ * status is TERMINAL. No event is accepted after that terminal.
+ */
+enum generation_feed_status {
+ GENERATION_PROTOCOL = -1,
+ GENERATION_MORE,
+ GENERATION_TERMINAL,
+ GENERATION_DRAIN
+};
+
+enum generation_outcome {
+ GENERATION_NORMAL,
+ GENERATION_LENGTH,
+ GENERATION_PROVIDER_ERROR,
+ GENERATION_INVALID
+};
+
+struct generation_usage {
+ int64_t input_tokens;
+ int64_t output_tokens;
+ int64_t cache_read;
+ int64_t cache_write;
+};
+
+struct generation_span {
+ const void *data;
+ size_t len;
+};
+
+/*
+ * A normalized, borrowed view of the event just accepted. Its fields are
+ * meaningful only when generation_feed() returns MORE or TERMINAL; DRAIN
+ * clears them. Spans have the lifetime of the raw payload passed to
+ * generation_feed(); the Generation independently copies everything it
+ * retains.
+ */
+struct generation_event {
+ enum generation_kind kind;
+ int index;
+ struct generation_span payload;
+ struct generation_span tool_id;
+ struct generation_span tool_name;
+ struct generation_usage usage;
+};
+
+/*
+ * generation_take() transfers assistant to the caller. reason remains
+ * borrowed from the Generation and is valid until generation_free().
+ */
+struct generation_result {
+ enum generation_outcome outcome;
+ struct msg *assistant;
+ struct generation_usage usage;
+ size_t data_used;
+ size_t tool_calls;
+ const char *reason;
+};
+
+/* data_available is the Turn's remaining shared text/argument byte budget. */
+struct generation *generation_new(size_t data_available);
+enum generation_feed_status generation_feed(struct generation *,
+ enum generation_kind, const void *, size_t,
+ struct generation_event *);
+/* Take exactly once, and only after generation_feed() returned TERMINAL. */
+int generation_take(struct generation *, struct generation_result *);
+const char *generation_error(const struct generation *);
+void generation_free(struct generation *);
+
+#endif /* GENERATION_H */
blob - af82dd62aa9b749fad477d3f73941e3ade196410
blob + 4bd62d0c749d40a8da35020c4cbce134e1103af7
--- src/fugu/Makefile
+++ src/fugu/Makefile
PROG= fugu
SRCS= main.c coord.c priv.c conf.c parse.y \
journal.c sysprompt.c tooldefs.c agentcfg.c skills.c output.c \
- anthropic_req.c openai_req.c msg.c \
+ anthropic_req.c openai_req.c generation.c msg.c \
json.c model_window.c buf.c imsgev.c log.c xmalloc.c
MAN= fugu.1 fugu.conf.5
MANDIR= ${PREFIX}/man/man
blob - b8171df9edc809073f9da40dde92cfca76440d02
blob + de55bf5a265ce25293d5f70f21fc28a557c18fe3
--- src/fugu-tool/Makefile
+++ src/fugu-tool/Makefile
PROG= fugu-tool
SRCS= main.c agent.c tools.c job.c supervisor.c worker.c imsgev.c \
- anthropic_req.c openai_req.c msg.c json.c buf.c diff.c log.c xmalloc.c
+ anthropic_req.c openai_req.c generation.c msg.c json.c buf.c diff.c log.c xmalloc.c
BINDIR= ${LIBEXECDIR}/fugu
MAN=