Totoro 593ba49458
feat(subagents): continue requested batch work and schedule explicit prerequisites (#7839)
* feat(subagents): continue native batches and honor item dependencies

* docs: link native batch follow-up changelog entries

* test(migrations): pin isolated history-seed upgrade to 0050

* fix(subagents): preserve follow-up scope, blockers and fair scheduling

* test(memory): freeze capacity evaluation time

* fix(subagents): guard follow-up admission and retain run controls

* fix(subagents): enforce runs:cancel on tool cancellation of follow-up batches

* style(tests): format test_batch_followup_host with ruff

* fix(subagents): drop the admission checkpoint check and keep SDK cancel compatible

The checkpoint check inside run admission waited on the checkpoint saver's
lock while holding the SQLite write lock, which could make another chat's
checkpoint write fail. Remove it; dispatch still checks the conversation
before admission.

A follow-up waiting for an unanswered question now keeps its claim until the
lease expires instead of rereading the conversation every poll. Cancel passes
the caller's run authority only to submitters that take it, so existing SDK
submitters keep the two-argument call. Document that retrying a prerequisite
by hand does not requeue its blocked items, and list 0052 as the migration head.

---------

Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
2026-11-05 15:10:21 +08:00

68 lines
1.8 KiB
Python

"""Process-local bridge from harness tools to the Gateway batch service."""
from __future__ import annotations
import threading
from dataclasses import dataclass
from typing import Any, NotRequired, Protocol, TypedDict
BATCH_CONTINUATION_NAMESPACE = "native-subagent-batches"
def batch_continuation_key(batch_id: str) -> str:
return "native-batch-followup:" + batch_id
class BatchItemInput(TypedDict):
key: str
prompt: str
acceptance_criteria: NotRequired[list[str] | None]
depends_on: NotRequired[list[str]]
@dataclass(frozen=True)
class BatchSubmitRequest:
user_id: str
thread_id: str
run_id: str | None
tool_call_id: str
submission_key: str
title: str
subagent_type: str
items: list[BatchItemInput]
max_live_items: int | None
max_running_items: int | None
execution_spec: dict[str, Any]
continuation: str | None = None
continuation_authority: Any = None
continuation_context: dict[str, Any] | None = None
class SubagentBatchSubmitter(Protocol):
async def submit(self, request: BatchSubmitRequest) -> dict[str, Any]: ...
async def get_batch(self, *, batch_id: str, user_id: str) -> dict[str, Any] | None: ...
async def read_batch_item(self, *, batch_id: str, user_id: str, thread_id: str, position: int) -> dict[str, Any] | None: ...
async def cancel_batch(self, *, batch_id: str, user_id: str) -> dict[str, Any] | None: ...
_submitter: SubagentBatchSubmitter | None = None
_lock = threading.Lock()
def set_subagent_batch_submitter(submitter: SubagentBatchSubmitter | None) -> None:
global _submitter
with _lock:
_submitter = submitter
def get_subagent_batch_submitter() -> SubagentBatchSubmitter | None:
with _lock:
return _submitter
def is_subagent_batch_runtime_available() -> bool:
return get_subagent_batch_submitter() is not None