feat(chat): stop an in-flight answer — Send becomes Stop, the partial is kept and persisted, the model stream is torn down

This commit is contained in:
2026-08-29 17:27:04 -04:00
parent 6bf7f456d4
commit 1a60ecbd8b
186 changed files with 1738 additions and 7796 deletions
+22 -10
View File
@@ -246,12 +246,19 @@ async def run_agent(
rounds = 0
while True:
calls: list[ToolCallPiece] = []
async for piece in llm.chat_stream(
cast("list[dict[str, str]]", messages), tools=tools
):
if isinstance(piece, ToolCallPiece):
calls.append(piece)
yield piece
# Phase 48: bind the round's stream so a consumer abandon
# (GeneratorExit into the yield below) tears down the in-flight
# model stream deterministically — not GC-dependent. Awaiting
# ``aclose()`` in the ``finally`` is safe because it does not
# yield; on a fully consumed round it is a quiet no-op.
stream = llm.chat_stream(cast("list[dict[str, str]]", messages), tools=tools)
try:
async for piece in stream:
if isinstance(piece, ToolCallPiece):
calls.append(piece)
yield piece
finally:
await stream.aclose()
if not calls:
return # the answer was streamed
call = calls[0] # a stream can carry several calls; run the first
@@ -287,8 +294,13 @@ async def run_agent(
"no-tools answer",
rounds,
)
async for piece in llm.chat_stream(
cast("list[dict[str, str]]", messages), tools=None
):
yield piece
# Phase 48: the forced final answer gets the same explicit
# teardown as the loop rounds (consumer abandon mid-final
# answer must still close the model's stream).
final = llm.chat_stream(cast("list[dict[str, str]]", messages), tools=None)
try:
async for piece in final:
yield piece
finally:
await final.aclose()
return