commit 1ea23e34d9f698ee5ed967301a83ed578127e7a4 from: Isaac Meerleo date: Wed Jul 15 15:17:50 2026 UTC add canonical generation module 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 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 blob - /dev/null blob + d54ec5cd03dff275f96e5e7eb5facf99b6a53e1b (mode 644) --- /dev/null +++ regress/generation/generation_test.c @@ -0,0 +1,760 @@ +/* + * Copyright (c) 2026 Isaac + * + * 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 + +#include +#include +#include + +#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 + * + * 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 + +#include +#include + +#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 + * + * 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 + +#include + +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=