Commit Diff


commit - 6bc0ac427c4a9ee19ec553c4faedfa250054ed10
commit + d8dc62cf90f48602dfdd56da2caf057b9fa09352
blob - e492b090e91a1319c6bc4b95fcd6f020d9306a1a
blob + 96dcda366b275e8e38ca8230bdc88b52c69dc117
--- regress/turn_mechanics/turn_mechanics_test.c
+++ regress/turn_mechanics/turn_mechanics_test.c
@@ -82,6 +82,20 @@ struct scripted_tool {
 	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;
@@ -107,11 +121,21 @@ struct fixture {
 	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;
@@ -119,6 +143,7 @@ struct fixture {
 	int			 add_usage_rc;
 	int			 accepts;
 	int			 projections;
+	uint64_t		 projection_revision;
 	int			 sent_callbacks;
 	int			 marking_sent;
 	int			 checkpoints;
@@ -127,6 +152,18 @@ struct fixture {
 };
 
 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;
@@ -285,6 +322,34 @@ fixture_tool(struct fixture *f)
 	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)
@@ -312,7 +377,8 @@ fixture_projection(void *arg, struct turn_projection *
 	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
@@ -321,11 +387,61 @@ fixture_projection_sent(void *arg, const struct turn_p
 	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)
 {
@@ -411,6 +527,7 @@ fixture_generate(void *arg, const struct turn_generati
 	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)
@@ -576,6 +693,7 @@ test_normal_text_completes(void)
 	    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 &&
@@ -621,6 +739,46 @@ test_subagent_normal_text_completes_without_conclusion
 }
 
 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\"}";
@@ -686,6 +844,58 @@ test_tool_results_are_paired_before_next_generation(vo
 }
 
 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 {
@@ -1123,6 +1333,122 @@ test_initial_checkpoint_precedes_first_projection(void
 }
 
 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 {
@@ -1808,6 +2134,334 @@ test_context_overflow_projects_to_turn_result(void)
 }
 
 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;
@@ -2022,7 +2676,9 @@ main(void)
 {
 	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();
@@ -2032,6 +2688,8 @@ main(void)
 	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();
@@ -2045,6 +2703,12 @@ main(void)
 	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
@@ -34,6 +34,7 @@ struct turn_state {
 	size_t				 data_used;
 	size_t				 tool_batches;
 	struct generation_usage		 usage;
+	int				 context_overflow_recovered;
 };
 
 struct turn_generation_io {
@@ -70,6 +71,9 @@ struct turn_tool_results {
 	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";
@@ -366,6 +370,64 @@ turn_generation_dispose(struct turn_generation_step *s
 }
 
 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)
 {
@@ -376,6 +438,9 @@ turn_generate(struct turn_state *turn, enum turn_tools
 	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;
@@ -429,6 +494,17 @@ turn_generate(struct turn_state *turn, enum turn_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
@@ -136,7 +136,7 @@ struct turn_result {
 	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;
@@ -147,14 +147,19 @@ struct turn_result {
 };
 
 /*
- * 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;
@@ -163,6 +168,10 @@ struct turn_adapter {
 	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);