Commit Diff


commit - 10b6dbfa3cb419e92d575e24b5f37c0748f97a97
commit + 1ea23e34d9f698ee5ed967301a83ed578127e7a4
blob - 2d9b5159b468ad7de6156909a7d880704f13c4ca
blob + 40d97bbcfbdff451c686c1743d770ecd648b8a54
--- regress/Makefile
+++ regress/Makefile
@@ -1,4 +1,4 @@
-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
@@ -44,7 +44,10 @@ I9  privileges only narrow		sandbox (each role reaches
 					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
@@ -63,6 +66,8 @@ I12 no ambient privilege		no setuid, no daemon, no soc
 					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,
@@ -161,6 +166,7 @@ src/common/log.c			log
 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)
@@ -173,8 +179,9 @@ src/fugu/agentcfg.c			agentcfg; turn (live schema/rout
 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
@@ -0,0 +1,4 @@
+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
@@ -0,0 +1,760 @@
+/*
+ * 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
@@ -0,0 +1,410 @@
+/*
+ * 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
@@ -0,0 +1,126 @@
+/*
+ * 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
@@ -1,7 +1,7 @@
 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
@@ -1,6 +1,6 @@
 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=