commit - 6bc0ac427c4a9ee19ec553c4faedfa250054ed10
commit + d8dc62cf90f48602dfdd56da2caf057b9fa09352
blob - e492b090e91a1319c6bc4b95fcd6f020d9306a1a
blob + 96dcda366b275e8e38ca8230bdc88b52c69dc117
--- regress/turn_mechanics/turn_mechanics_test.c
+++ regress/turn_mechanics/turn_mechanics_test.c
int fill_error;
};
+struct scripted_preflight {
+ enum turn_tools_mode tools;
+ struct generation_usage usage;
+ enum turn_io io;
+ const char *error;
+ int fill_error;
+};
+
+struct observed_preflight {
+ struct generation_usage usage;
+ size_t data_used;
+ size_t tool_batches;
+};
+
struct observed_assistant {
const struct msg *message;
enum generation_outcome outcome;
struct scripted_tool tools[SCRIPT_TOOLS_MAX];
size_t ntools;
size_t tool_next;
+ struct scripted_preflight prepares[SCRIPT_GENERATIONS_MAX];
+ size_t nprepares;
+ size_t prepare_next;
+ struct scripted_preflight recoveries[SCRIPT_GENERATIONS_MAX];
+ size_t nrecoveries;
+ size_t recovery_next;
+ struct observed_preflight prepare_observed[SCRIPT_GENERATIONS_MAX];
+ struct observed_preflight recovery_observed[SCRIPT_GENERATIONS_MAX];
+ struct turn_result *running_result;
struct observed_assistant assistants[SCRIPT_GENERATIONS_MAX];
size_t nassistants;
struct observed_batch batches[SCRIPT_TOOLS_MAX];
size_t nbatches;
size_t projection_count[SCRIPT_GENERATIONS_MAX];
+ uint64_t projection_receipt[SCRIPT_GENERATIONS_MAX];
enum turn_tools_mode request_tools[SCRIPT_GENERATIONS_MAX];
int set_result_rc[SCRIPT_RESULTS_MAX];
size_t nset_result_rc;
int add_usage_rc;
int accepts;
int projections;
+ uint64_t projection_revision;
int sent_callbacks;
int marking_sent;
int checkpoints;
};
static void
+observe_preflight(struct observed_preflight *observed,
+ const struct turn_result *result)
+{
+ memset(observed, 0, sizeof(*observed));
+ if (result == NULL)
+ return;
+ observed->usage = result->usage;
+ observed->data_used = result->data_used;
+ observed->tool_batches = result->tool_batches;
+}
+
+static void
copy_string(char *dst, size_t dstsz, const char *src)
{
size_t len;
return (tool);
}
+static struct scripted_preflight *
+fixture_prepare(struct fixture *f, enum turn_tools_mode tools)
+{
+ struct scripted_preflight *script;
+
+ if (f->nprepares >= SCRIPT_GENERATIONS_MAX)
+ abort();
+ script = &f->prepares[f->nprepares++];
+ memset(script, 0, sizeof(*script));
+ script->tools = tools;
+ script->io = TURN_IO_OK;
+ return (script);
+}
+
+static struct scripted_preflight *
+fixture_recovery(struct fixture *f, enum turn_tools_mode tools)
+{
+ struct scripted_preflight *script;
+
+ if (f->nrecoveries >= SCRIPT_GENERATIONS_MAX)
+ abort();
+ script = &f->recoveries[f->nrecoveries++];
+ memset(script, 0, sizeof(*script));
+ script->tools = tools;
+ script->io = TURN_IO_OK;
+ return (script);
+}
+
static void
script_result(struct scripted_tool *tool, size_t ordinal, const char *id,
const void *data, size_t len, int is_error)
projection->messages = &f->accepted;
projection->message_count = message_count(&f->accepted);
projection->cache_tail_msg = -1;
- projection->receipt = projection->message_count + 100;
+ projection->receipt = f->projection_revision * 100 +
+ projection->message_count;
}
static void
struct fixture *f = arg;
if (!f->marking_sent || projection->receipt !=
- projection->message_count + 100)
+ f->projection_revision * 100 + projection->message_count)
f->bad = 1;
f->sent_callbacks++;
}
+static enum turn_io
+fixture_prepare_generation(void *arg, enum turn_tools_mode tools,
+ struct generation_usage *usage, char *error, size_t errorsz)
+{
+ struct fixture *f = arg;
+ struct scripted_preflight *script;
+
+ if (f->prepare_next >= f->nprepares) {
+ f->bad = 1;
+ return (TURN_IO_ERROR);
+ }
+ observe_preflight(&f->prepare_observed[f->prepare_next],
+ f->running_result);
+ script = &f->prepares[f->prepare_next++];
+ if (tools != script->tools)
+ f->bad = 1;
+ *usage = script->usage;
+ f->projection_revision++;
+ if (script->error != NULL && errorsz > 0)
+ (void)snprintf(error, errorsz, "%s", script->error);
+ if (script->fill_error && errorsz > 0)
+ memset(error, 'P', errorsz);
+ return (script->io);
+}
+
+static enum turn_io
+fixture_recover_context_overflow(void *arg, enum turn_tools_mode tools,
+ struct generation_usage *usage, char *error, size_t errorsz)
+{
+ struct fixture *f = arg;
+ struct scripted_preflight *script;
+
+ if (f->recovery_next >= f->nrecoveries) {
+ f->bad = 1;
+ return (TURN_IO_ERROR);
+ }
+ observe_preflight(&f->recovery_observed[f->recovery_next],
+ f->running_result);
+ script = &f->recoveries[f->recovery_next++];
+ if (tools != script->tools)
+ f->bad = 1;
+ *usage = script->usage;
+ f->projection_revision++;
+ if (script->error != NULL && errorsz > 0)
+ (void)snprintf(error, errorsz, "%s", script->error);
+ if (script->fill_error && errorsz > 0)
+ memset(error, 'R', errorsz);
+ return (script->io);
+}
+
static void
fixture_accept(void *arg, struct msg *message)
{
slot = f->generation_next;
script = &f->generations[f->generation_next++];
f->projection_count[slot] = request->projection.message_count;
+ f->projection_receipt[slot] = request->projection.receipt;
f->request_tools[slot] = request->tools;
if (request->projection.messages != &f->accepted ||
request->tools != script->tools)
result.usage.cache_write == 0);
CHECK(f.generation_next == 1 && f.accepts == 1 &&
f.sent_callbacks == 1 && f.nassistants == 1 &&
+ f.projections == 1 && f.checkpoints == 4 &&
f.request_tools[0] == TURN_TOOLS_ENABLED &&
f.projection_count[0] == 1 && message_count(&f.accepted) == 2);
CHECK(f.assistants[0].outcome == GENERATION_NORMAL &&
}
static void
+test_prepare_precedes_tools_disabled_conclusion(void)
+{
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ f.running_result = &result;
+ adapter.prepare_generation = fixture_prepare_generation;
+ (void)fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "partial");
+ script_done(generation, "max_tokens");
+ (void)fixture_prepare(&f, TURN_TOOLS_DISABLED);
+ generation = fixture_generation(&f, TURN_TOOLS_DISABLED);
+ script_text(generation, "final");
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_SUBAGENT, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 &&
+ result.status == TURN_STATUS_CONCLUDED &&
+ result.failure == TURN_FAILURE_NONE &&
+ strcmp(result.stop_reason, "stop") == 0);
+ CHECK(f.prepare_next == 2 && f.generation_next == 2 &&
+ f.projections == 2 && f.request_tools[0] == TURN_TOOLS_ENABLED &&
+ f.request_tools[1] == TURN_TOOLS_DISABLED &&
+ f.projection_receipt[0] == 101 &&
+ f.projection_receipt[1] == 203);
+ CHECK(f.prepare_observed[0].data_used == 1 &&
+ f.prepare_observed[1].data_used == 8 &&
+ f.prepare_observed[1].tool_batches == 0 &&
+ f.accepts == 3 && f.nassistants == 2 && f.tool_next == 0);
+ msglist_free(&f.accepted);
+}
+
+static void
test_tool_results_are_paired_before_next_generation(void)
{
static const char input[] = "{\"path\":\"x\"}";
}
static void
+test_prepare_precedes_each_fresh_projection(void)
+{
+ static const struct generation_usage first = { 1, 2, 3, 4 };
+ static const struct generation_usage second = { 10, 20, 30, 40 };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_preflight *prepare;
+ struct scripted_tool *tool;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ f.running_result = &result;
+ adapter.prepare_generation = fixture_prepare_generation;
+ prepare = fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ prepare->usage = first;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_tool_begin(generation, 0, "prepared", "read");
+ script_done(generation, "tool_use");
+ tool = fixture_tool(&f);
+ script_result(tool, 0, NULL, "ok", 2, 0);
+ prepare = fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ prepare->usage = second;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "done");
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_DONE &&
+ result.failure == TURN_FAILURE_NONE && result.tool_batches == 1);
+ CHECK(f.prepare_next == 2 && f.projections == 2 &&
+ f.generation_next == 2 && f.tool_next == 1 &&
+ f.projection_count[0] == 1 && f.projection_count[1] == 3 &&
+ f.projection_receipt[0] == 101 &&
+ f.projection_receipt[1] == 203);
+ CHECK(result.usage.input_tokens == 11 &&
+ result.usage.output_tokens == 22 &&
+ result.usage.cache_read == 33 &&
+ result.usage.cache_write == 44);
+ CHECK(f.prepare_observed[0].data_used == 1 &&
+ f.prepare_observed[0].tool_batches == 0 &&
+ f.prepare_observed[0].usage.input_tokens == 0 &&
+ f.prepare_observed[1].data_used == 5 &&
+ f.prepare_observed[1].tool_batches == 1 &&
+ f.prepare_observed[1].usage.input_tokens == 1);
+ msglist_free(&f.accepted);
+}
+
+static void
test_invalid_generations_are_never_accepted(void)
{
static const struct {
}
static void
+test_prepare_callback_terminal_results(void)
+{
+ enum prepare_case {
+ PREPARE_ERROR,
+ PREPARE_INTERRUPTED,
+ PREPARE_CANCELLED,
+ PREPARE_FILLED_ERROR
+ };
+ static const struct generation_usage nested = { 5, 6, 7, 8 };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_preflight *prepare;
+ int which, rc;
+
+ for (which = PREPARE_ERROR; which <= PREPARE_FILLED_ERROR; which++) {
+ fixture_init(&f, &adapter, "q");
+ adapter.prepare_generation = fixture_prepare_generation;
+ prepare = fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ prepare->usage = nested;
+ prepare->io = TURN_IO_ERROR;
+ prepare->error = "preparation summary failed";
+ if (which == PREPARE_INTERRUPTED)
+ prepare->io = TURN_IO_INTERRUPTED;
+ else if (which == PREPARE_CANCELLED)
+ f.interrupt_checkpoint = 2;
+ else if (which == PREPARE_FILLED_ERROR) {
+ prepare->error = NULL;
+ prepare->fill_error = 1;
+ }
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "must not run");
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 &&
+ result.data_used == 1 && result.assistant == NULL &&
+ f.prepare_next == 1 && f.projections == 0 &&
+ f.generation_next == 0 && f.sent_callbacks == 0 &&
+ f.accepts == 0 && f.checkpoints ==
+ (which == PREPARE_INTERRUPTED ? 1 : 2));
+ CHECK(result.usage.input_tokens == 5 &&
+ result.usage.output_tokens == 6 &&
+ result.usage.cache_read == 7 &&
+ result.usage.cache_write == 8);
+ if (which == PREPARE_INTERRUPTED ||
+ which == PREPARE_CANCELLED) {
+ CHECK(result.status == TURN_STATUS_INTERRUPTED &&
+ result.failure == TURN_FAILURE_NONE &&
+ result.error[0] == '\0');
+ } else if (which == PREPARE_FILLED_ERROR) {
+ CHECK(result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_ADAPTER &&
+ result.error[0] == 'P' &&
+ result.error[TURN_ERROR_MAX - 2] == 'P' &&
+ result.error[TURN_ERROR_MAX - 1] == '\0');
+ } else {
+ CHECK(result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_ADAPTER &&
+ strcmp(result.error,
+ "preparation summary failed") == 0);
+ }
+ msglist_free(&f.accepted);
+ }
+}
+
+static void
+test_prepare_usage_obeys_whole_turn_bound(void)
+{
+ static const struct generation_usage full = {
+ GENERATION_USAGE_MAX, 0, 0, 0
+ };
+ static const struct generation_usage one = { 1, 0, 0, 0 };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_preflight *prepare;
+ struct scripted_tool *tool;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ adapter.prepare_generation = fixture_prepare_generation;
+ prepare = fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ prepare->usage = full;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_tool_begin(generation, 0, "prepare-bound", "read");
+ script_done(generation, "tool_use");
+ tool = fixture_tool(&f);
+ script_result(tool, 0, NULL, "ok", 2, 0);
+ prepare = fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ prepare->usage = one;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "must not run");
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_USAGE_BOUND &&
+ result.invalid_cause == GENERATION_INVALID_USAGE_BOUND &&
+ result.usage.input_tokens == GENERATION_USAGE_MAX &&
+ result.usage.output_tokens == 0 && result.data_used == 5 &&
+ result.tool_batches == 1 && result.error[0] != '\0');
+ CHECK(f.prepare_next == 2 && f.projections == 1 &&
+ f.generation_next == 1 && f.tool_next == 1 && f.nbatches == 1 &&
+ f.accepts == 2 && f.nassistants == 1 &&
+ f.projection_receipt[0] == 101);
+ msglist_free(&f.accepted);
+}
+
+static void
test_generation_helper_misuse_is_rejected(void)
{
enum helper_misuse {
}
static void
+test_lead_recovers_one_precontent_context_overflow(void)
+{
+ static const struct generation_usage rejected = { 1, 2, 3, 4 };
+ static const struct generation_usage recovery = { 10, 20, 30, 40 };
+ static const struct generation_usage retried = { 100, 200, 300, 400 };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_preflight *preflight;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ f.running_result = &result;
+ adapter.recover_context_overflow = fixture_recover_context_overflow;
+ preflight = fixture_recovery(&f, TURN_TOOLS_ENABLED);
+ preflight->usage = recovery;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_usage(generation, &rejected);
+ script_context_overflow(generation, "too large");
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "answer");
+ script_usage(generation, &retried);
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_DONE &&
+ result.failure == TURN_FAILURE_NONE && result.data_used == 7 &&
+ result.assistant != NULL && strcmp(result.stop_reason, "stop") == 0);
+ CHECK(f.recovery_next == 1 && f.generation_next == 2 &&
+ f.projections == 2 && f.projection_receipt[0] == 1 &&
+ f.projection_receipt[1] == 101 && f.sent_callbacks == 2 &&
+ f.accepts == 1 && f.nassistants == 1 && f.tool_next == 0);
+ CHECK(result.usage.input_tokens == 111 &&
+ result.usage.output_tokens == 222 &&
+ result.usage.cache_read == 333 &&
+ result.usage.cache_write == 444);
+ CHECK(f.recovery_observed[0].data_used == 1 &&
+ f.recovery_observed[0].tool_batches == 0 &&
+ f.recovery_observed[0].usage.input_tokens == 1 &&
+ f.recovery_observed[0].usage.output_tokens == 2 &&
+ f.recovery_observed[0].usage.cache_read == 3 &&
+ f.recovery_observed[0].usage.cache_write == 4);
+ msglist_free(&f.accepted);
+}
+
+static void
+test_post_tool_overflow_retries_without_reexecuting_tools(void)
+{
+ static const struct generation_usage prepare_first = { 1, 0, 0, 0 };
+ static const struct generation_usage prepare_next = { 2, 0, 0, 0 };
+ static const struct generation_usage rejected = { 4, 0, 0, 0 };
+ static const struct generation_usage recovery = { 8, 0, 0, 0 };
+ static const struct generation_usage retried = { 16, 0, 0, 0 };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_preflight *preflight;
+ struct scripted_tool *tool;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ f.running_result = &result;
+ adapter.prepare_generation = fixture_prepare_generation;
+ adapter.recover_context_overflow = fixture_recover_context_overflow;
+ preflight = fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ preflight->usage = prepare_first;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_tool_begin(generation, 0, "once", "read");
+ script_done(generation, "tool_use");
+ tool = fixture_tool(&f);
+ script_result(tool, 0, NULL, "ok", 2, 0);
+ preflight = fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ preflight->usage = prepare_next;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_usage(generation, &rejected);
+ script_context_overflow(generation, "continuation too large");
+ preflight = fixture_recovery(&f, TURN_TOOLS_ENABLED);
+ preflight->usage = recovery;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "final");
+ script_usage(generation, &retried);
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_LEAD, 1, 3);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_DONE &&
+ result.failure == TURN_FAILURE_NONE && result.data_used == 10 &&
+ result.tool_batches == 1 && result.usage.input_tokens == 31 &&
+ strcmp(result.stop_reason, "stop") == 0);
+ CHECK(f.prepare_next == 2 && f.recovery_next == 1 &&
+ f.generation_next == 3 && f.projections == 3 &&
+ f.projection_count[0] == 1 && f.projection_count[1] == 3 &&
+ f.projection_count[2] == 3 && f.projection_receipt[0] == 101 &&
+ f.projection_receipt[1] == 203 &&
+ f.projection_receipt[2] == 303);
+ CHECK(f.tool_next == 1 && f.nbatches == 1 && f.accepts == 3 &&
+ f.nassistants == 2 && f.sent_callbacks == 3 &&
+ f.prepare_observed[1].data_used == 5 &&
+ f.prepare_observed[1].tool_batches == 1 &&
+ f.prepare_observed[1].usage.input_tokens == 1 &&
+ f.recovery_observed[0].data_used == 5 &&
+ f.recovery_observed[0].tool_batches == 1 &&
+ f.recovery_observed[0].usage.input_tokens == 7);
+ msglist_free(&f.accepted);
+}
+
+static void
+test_ineligible_provider_failures_do_not_recover(void)
+{
+ enum ineligible_case {
+ INELIGIBLE_GENERIC,
+ INELIGIBLE_POST_CONTENT,
+ INELIGIBLE_SUBAGENT
+ };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ enum provider_error_category want_category;
+ enum turn_policy_kind policy;
+ int which, rc;
+
+ for (which = INELIGIBLE_GENERIC; which <= INELIGIBLE_SUBAGENT;
+ which++) {
+ fixture_init(&f, &adapter, "q");
+ adapter.recover_context_overflow =
+ fixture_recover_context_overflow;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ want_category = PROVIDER_ERROR_CONTEXT_OVERFLOW;
+ policy = TURN_POLICY_LEAD;
+ if (which == INELIGIBLE_GENERIC) {
+ script_provider_error(generation, "generic failure");
+ want_category = PROVIDER_ERROR_OTHER;
+ } else {
+ if (which == INELIGIBLE_POST_CONTENT)
+ script_text(generation, "x");
+ else
+ policy = TURN_POLICY_SUBAGENT;
+ script_context_overflow(generation, "typed overflow");
+ }
+ options_init(&options, policy, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 &&
+ result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_PROVIDER &&
+ result.provider_error_category == want_category &&
+ result.provider_content_accepted ==
+ (which == INELIGIBLE_POST_CONTENT) &&
+ result.data_used ==
+ (which == INELIGIBLE_POST_CONTENT ? 2 : 1));
+ CHECK(f.recovery_next == 0 && f.generation_next == 1 &&
+ f.projections == 1 && f.sent_callbacks == 1 &&
+ f.accepts == 0 && f.nassistants == 0 && f.tool_next == 0);
+ msglist_free(&f.accepted);
+ }
+}
+
+static void
+test_context_overflow_recovery_is_turn_wide_once(void)
+{
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_tool *tool;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ adapter.prepare_generation = fixture_prepare_generation;
+ adapter.recover_context_overflow = fixture_recover_context_overflow;
+ (void)fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ (void)fixture_prepare(&f, TURN_TOOLS_ENABLED);
+ (void)fixture_recovery(&f, TURN_TOOLS_ENABLED);
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_context_overflow(generation, "first overflow");
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_tool_begin(generation, 0, "after-recovery", "read");
+ script_done(generation, "tool_use");
+ tool = fixture_tool(&f);
+ script_result(tool, 0, NULL, "ok", 2, 0);
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_context_overflow(generation, "second overflow");
+ options_init(&options, TURN_POLICY_LEAD, 1, 3);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_PROVIDER &&
+ result.provider_error_category == PROVIDER_ERROR_CONTEXT_OVERFLOW &&
+ result.provider_content_accepted == 0 && result.data_used == 5 &&
+ result.tool_batches == 1 &&
+ strcmp(result.error, "second overflow") == 0);
+ CHECK(f.prepare_next == 2 && f.recovery_next == 1 &&
+ f.generation_next == 3 && f.projections == 3 &&
+ f.projection_receipt[0] == 101 &&
+ f.projection_receipt[1] == 201 &&
+ f.projection_receipt[2] == 303 && f.tool_next == 1 &&
+ f.nbatches == 1 && f.accepts == 2 && f.nassistants == 1 &&
+ f.sent_callbacks == 3);
+ msglist_free(&f.accepted);
+}
+
+static void
+test_recovery_callback_terminal_results(void)
+{
+ enum recovery_case {
+ RECOVERY_ERROR,
+ RECOVERY_INTERRUPTED,
+ RECOVERY_CANCELLED,
+ RECOVERY_FILLED_ERROR
+ };
+ static const struct generation_usage rejected = { 1, 2, 3, 4 };
+ static const struct generation_usage nested = { 5, 6, 7, 8 };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_preflight *recovery;
+ int which, rc;
+
+ for (which = RECOVERY_ERROR; which <= RECOVERY_FILLED_ERROR; which++) {
+ fixture_init(&f, &adapter, "q");
+ adapter.recover_context_overflow =
+ fixture_recover_context_overflow;
+ recovery = fixture_recovery(&f, TURN_TOOLS_ENABLED);
+ recovery->usage = nested;
+ recovery->io = TURN_IO_ERROR;
+ recovery->error = "recovery summary failed";
+ if (which == RECOVERY_INTERRUPTED)
+ recovery->io = TURN_IO_INTERRUPTED;
+ else if (which == RECOVERY_CANCELLED) {
+ recovery->io = TURN_IO_OK;
+ recovery->error = NULL;
+ f.interrupt_checkpoint = 3;
+ } else if (which == RECOVERY_FILLED_ERROR) {
+ recovery->error = NULL;
+ recovery->fill_error = 1;
+ }
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_usage(generation, &rejected);
+ script_context_overflow(generation, "too large");
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "must not retry");
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 && result.data_used == 1 &&
+ result.assistant == NULL && f.recovery_next == 1 &&
+ f.projections == 1 && f.generation_next == 1 &&
+ f.sent_callbacks == 1 && f.accepts == 0 && f.checkpoints ==
+ (which == RECOVERY_INTERRUPTED ? 2 : 3));
+ CHECK(result.usage.input_tokens == 6 &&
+ result.usage.output_tokens == 8 &&
+ result.usage.cache_read == 10 &&
+ result.usage.cache_write == 12);
+ if (which == RECOVERY_INTERRUPTED ||
+ which == RECOVERY_CANCELLED) {
+ CHECK(result.status == TURN_STATUS_INTERRUPTED &&
+ result.failure == TURN_FAILURE_NONE &&
+ result.provider_error_category == PROVIDER_ERROR_NONE &&
+ result.error[0] == '\0');
+ } else if (which == RECOVERY_FILLED_ERROR) {
+ CHECK(result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_ADAPTER &&
+ result.error[0] == 'R' &&
+ result.error[TURN_ERROR_MAX - 2] == 'R' &&
+ result.error[TURN_ERROR_MAX - 1] == '\0');
+ } else {
+ CHECK(result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_ADAPTER &&
+ strcmp(result.error, "recovery summary failed") == 0);
+ }
+ msglist_free(&f.accepted);
+ }
+}
+
+static void
+test_recovery_usage_obeys_whole_turn_bound(void)
+{
+ static const struct generation_usage full = {
+ GENERATION_USAGE_MAX, 0, 0, 0
+ };
+ static const struct generation_usage one = { 1, 0, 0, 0 };
+ struct fixture f;
+ struct turn_adapter adapter;
+ struct turn_options options;
+ struct turn_result result;
+ struct scripted_generation *generation;
+ struct scripted_preflight *recovery;
+ int rc;
+
+ fixture_init(&f, &adapter, "q");
+ f.running_result = &result;
+ adapter.recover_context_overflow = fixture_recover_context_overflow;
+ recovery = fixture_recovery(&f, TURN_TOOLS_ENABLED);
+ recovery->usage = one;
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_usage(generation, &full);
+ script_context_overflow(generation, "too large");
+ generation = fixture_generation(&f, TURN_TOOLS_ENABLED);
+ script_text(generation, "must not retry");
+ script_done(generation, "stop");
+ options_init(&options, TURN_POLICY_LEAD, 1, 2);
+ rc = turn_run(&adapter, &options, &result);
+
+ CHECK(rc == 0 && f.bad == 0 && result.status == TURN_STATUS_FAILED &&
+ result.failure == TURN_FAILURE_USAGE_BOUND &&
+ result.invalid_cause == GENERATION_INVALID_USAGE_BOUND &&
+ result.usage.input_tokens == GENERATION_USAGE_MAX &&
+ result.usage.output_tokens == 0 && result.data_used == 1 &&
+ result.tool_batches == 0 && result.error[0] != '\0');
+ CHECK(f.recovery_next == 1 && f.projections == 1 &&
+ f.generation_next == 1 && f.sent_callbacks == 1 &&
+ f.accepts == 0 && f.nassistants == 0 &&
+ f.recovery_observed[0].usage.input_tokens == GENERATION_USAGE_MAX);
+ msglist_free(&f.accepted);
+}
+
+static void
test_out_of_order_results_are_accepted_in_call_order(void)
{
struct fixture f;
{
test_normal_text_completes();
test_subagent_normal_text_completes_without_conclusion();
+ test_prepare_precedes_tools_disabled_conclusion();
test_tool_results_are_paired_before_next_generation();
+ test_prepare_precedes_each_fresh_projection();
test_invalid_generations_are_never_accepted();
test_no_argument_call_uses_canonical_empty_object();
test_lead_length_text_completes_and_calls_continue();
test_lead_steers_do_not_reset_tool_batch_limit();
test_subagent_cap_gets_one_tools_disabled_conclusion();
test_initial_checkpoint_precedes_first_projection();
+ test_prepare_callback_terminal_results();
+ test_prepare_usage_obeys_whole_turn_bound();
test_generation_helper_misuse_is_rejected();
test_adapter_interrupted_results_are_authoritative();
test_terminal_generation_is_checkpointed_before_accept();
test_fixed_conclusion_guidance_is_outside_hostile_data_bound();
test_provider_and_adapter_errors_remain_distinct();
test_context_overflow_projects_to_turn_result();
+ test_lead_recovers_one_precontent_context_overflow();
+ test_post_tool_overflow_retries_without_reexecuting_tools();
+ test_ineligible_provider_failures_do_not_recover();
+ test_context_overflow_recovery_is_turn_wide_once();
+ test_recovery_callback_terminal_results();
+ test_recovery_usage_obeys_whole_turn_bound();
test_out_of_order_results_are_accepted_in_call_order();
test_incomplete_or_inconsistent_result_batches_are_rejected();
test_turn_data_and_usage_are_accumulated_once();
blob - eb43c47a6b721daed18e537eccde0b3a9d378c29
blob + fac8235f9b47046f635279d4fff54902989fe1e8
--- src/common/turn_mechanics.c
+++ src/common/turn_mechanics.c
size_t data_used;
size_t tool_batches;
struct generation_usage usage;
+ int context_overflow_recovered;
};
struct turn_generation_io {
char error[TURN_ERROR_MAX];
};
+typedef enum turn_io (*turn_preflight_fn)(void *, enum turn_tools_mode,
+ struct generation_usage *, char *, size_t);
+
static const char turn_subagent_dangling[] =
"subagent tool call was truncated and was not executed";
static const char turn_subagent_bound[] = "subagent data bound exceeded";
}
static int
+turn_preflight(struct turn_state *turn, enum turn_tools_mode tools,
+ turn_preflight_fn callback, const char *fallback_error,
+ const char *usage_error)
+{
+ struct generation_usage usage;
+ char error[TURN_ERROR_MAX];
+ enum turn_io status;
+ int usage_failed;
+
+ memset(&usage, 0, sizeof(usage));
+ error[0] = '\0';
+ turn_result_account(turn);
+ status = callback(turn->adapter->arg, tools, &usage, error,
+ sizeof(error));
+ error[sizeof(error) - 1] = '\0';
+ usage_failed = turn_usage_add(&turn->usage, &usage) == -1;
+ turn_result_account(turn);
+ if (status == TURN_IO_INTERRUPTED) {
+ turn_interrupt(turn);
+ return (-1);
+ }
+ if (turn_checkpoint(turn) != 0)
+ return (-1);
+ if (usage_failed) {
+ turn_fail(turn, TURN_FAILURE_USAGE_BOUND,
+ GENERATION_INVALID_USAGE_BOUND, usage_error);
+ return (-1);
+ }
+ if (status != TURN_IO_OK) {
+ turn_fail(turn, TURN_FAILURE_ADAPTER, GENERATION_INVALID_NONE,
+ error[0] != '\0' ? error : fallback_error);
+ return (-1);
+ }
+ return (0);
+}
+
+static int
+turn_prepare_generation(struct turn_state *turn, enum turn_tools_mode tools)
+{
+ if (turn->adapter->prepare_generation == NULL)
+ return (0);
+ return (turn_preflight(turn, tools,
+ turn->adapter->prepare_generation, "Generation preparation failed",
+ "Generation preparation usage exceeded the Turn usage bound"));
+}
+
+static int
+turn_recover_context_overflow(struct turn_state *turn,
+ enum turn_tools_mode tools)
+{
+ turn->context_overflow_recovered = 1;
+ return (turn_preflight(turn, tools,
+ turn->adapter->recover_context_overflow,
+ "context-overflow recovery failed",
+ "context-overflow recovery usage exceeded the Turn usage bound"));
+}
+
+static int
turn_generate(struct turn_state *turn, enum turn_tools_mode tools,
struct turn_generation_step *step)
{
int data_failed, usage_failed;
memset(step, 0, sizeof(*step));
+ if (turn_prepare_generation(turn, tools) == -1)
+ return (-1);
+retry:
memset(&request, 0, sizeof(request));
turn->adapter->projection(turn->adapter->arg, &request.projection);
request.tools = tools;
return (-1);
}
if (step->result.outcome == GENERATION_PROVIDER_ERROR) {
+ if (step->result.provider_error_category ==
+ PROVIDER_ERROR_CONTEXT_OVERFLOW &&
+ !step->result.provider_content_accepted &&
+ turn->options->policy == TURN_POLICY_LEAD &&
+ turn->adapter->recover_context_overflow != NULL &&
+ !turn->context_overflow_recovered) {
+ turn_generation_dispose(step);
+ if (turn_recover_context_overflow(turn, tools) == -1)
+ return (-1);
+ goto retry;
+ }
turn_fail_provider(turn, &step->result);
return (-1);
}
blob - 355785fb1a00449bdf44276859d9a7f026b35e22
blob + c5ebdac298d608821223488cc2d5277a259f61ed
--- src/common/turn_mechanics.h
+++ src/common/turn_mechanics.h
enum generation_invalid_cause conclusion_invalid_cause;
enum provider_error_category conclusion_provider_error_category;
int conclusion_provider_content_accepted;
- /* Completed Generations plus nested usage reported by execute(). */
+ /* Completed Generations plus nested usage reported by Adapter callbacks. */
struct generation_usage usage;
size_t data_used;
size_t tool_batches;
};
/*
- * generate() and execute() are blocking Adapter calls. Their io/result
- * handles are borrowed only for the duration of the callback. accept()
- * takes ownership of its message. checkpoint() may be NULL. generate()
- * marks a completely queued Projection exactly once, then feeds through a
- * terminal event before returning OK. execute() sets exactly one matching
- * result for every borrowed call ordinal; it may fill ordinals in any order.
- * The first checkpoint precedes the first Projection; further checkpoints
- * surround blocking callbacks and accepted messages.
+ * prepare_generation(), recover_context_overflow(), generate(), and execute()
+ * are blocking Adapter calls. The two preflight callbacks are optional,
+ * precede borrowing the Projection they may replace, and receive zeroed usage
+ * output for nested Provider work only. prepare_generation() runs before
+ * each prospective Generation except the immediate retry prepared by a
+ * successful recovery. Their io/result handles are borrowed only for the
+ * duration of the callback. accept() takes ownership of its message.
+ * checkpoint() may be NULL. generate() marks a completely queued Projection
+ * exactly once, then feeds through a terminal event before returning OK.
+ * execute() sets exactly one matching result for every borrowed call ordinal;
+ * it may fill ordinals in any order. The first checkpoint precedes the first
+ * Projection; further checkpoints surround blocking callbacks and accepted
+ * messages.
*/
struct turn_adapter {
void *arg;
void (*accept)(void *, struct msg *);
void (*assistant_accepted)(void *, const struct turn_assistant_event *);
enum turn_checkpoint (*checkpoint)(void *);
+ enum turn_io (*prepare_generation)(void *, enum turn_tools_mode,
+ struct generation_usage *, char *, size_t);
+ enum turn_io (*recover_context_overflow)(void *, enum turn_tools_mode,
+ struct generation_usage *, char *, size_t);
enum turn_io (*generate)(void *,
const struct turn_generation_request *, struct turn_generation_io *,
char *, size_t);