# MMSP-RFC-0008 请求中断

Source: https://mmsp.penguin.ooo/zh/rfcs/mmsp-rfc-0008-request-interruption/

- Feature Name: `request_interruption`
- Start Date: 2026-10-24
- RFC PR: [#301](https://github.com/Prism-Shadow/model-message-stream-protocol/pull/301)
- Kind: spec

## Abstract

调用方能在任何时候中止信号或关闭流来中断一个请求，发往服务商的请求随之立即关闭，两个包的每个客户端都是如此，唯一的例外见 §3。本文规定调用方怎样中断、会收到什么，两种语言各自怎样关闭发往服务商的请求，被中断的有状态调用记录什么，以及服务端、playground 与 `mmsp` 客户端怎样传递中断。

## 1. Introduction

项目负责人的决定是「允许立即中断」：调用方能在任何时候停下一条流，发往服务商的请求随之结束。在 [#292](https://github.com/Prism-Shadow/model-message-stream-protocol/pull/292) 之前：
- 在 openai 或 google-genai SDK 上关闭或离开一条 Python 流，连接要等垃圾回收运行时才关闭；
- 调用方忙于别的事时，或另一个任务正在读取时到来的中止，要等到下一次读取或下一个事件才生效；
- 在 openai 或 anthropic SDK 上被中止的 TypeScript 流，结束得像一条中途断开的流：先是 `.done` 内容项，再是 `IncompleteStreamError`；用量已经到达时，则是一个 `stop` 和一条被记录的回合。

因此在 Python 中，playground 的 Stop，以及调用方已经离开的服务端，会让上游请求再开着几秒钟，或者在服务商沉默期间一直开着。本规范在 [#298](https://github.com/Prism-Shadow/model-message-stream-protocol/pull/298) 中写作 MMSP-RFC-0012。

## 2. Conventions and Terminology

本文中的关键词“必须”“不得”“应当”“不应”“可以”依次对应 BCP 14（[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119)、[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174)）中的 MUST、MUST NOT、SHOULD、SHOULD NOT 与 MAY，按其定义解释；本文只在这一含义上使用这五个词。

- *信号*：调用时传入的 `AbortSignal`，Python 中来自 `mmsp.abort_signal`，TypeScript 中是平台自带的。
- *中止*：Python 中是 `signal.abort(reason)`；TypeScript 中是在拥有该信号的 `AbortController` 上调用 `abort(reason)`。
- *关闭*：在流的生成器上调用 `aclose()` / `return()`，或者离开读取它的循环（在 Python 中，要等再没有别的东西持有这个生成器）。
- *中止错误*：Python 中是携带信号原因的 `asyncio.CancelledError`；TypeScript 中是信号的 `reason`。
- *未完成的内容项*：增量还在流式到达、尚无 `.done` 的那个内容项。

## 3. 中断一条流

- 调用方可以在任何时候中止信号或关闭流来中断它。发往服务商的请求必须随之立即关闭，只有一种情况例外：调用方在两次读取之间忙于别的事时中止 Python 的 `mmsp` 客户端，它要到调用方下一次读取时才关闭请求（§4）。
- 中止之后，调用方必须先拿到已经到达的增量，然后拿到中止错误；被关闭的生成器不再产出任何东西。调用方不得拿到未完成内容项的 `.done` 或 `stop`，或是残缺的 `.done` 之后跟着的 `IncompleteStreamError`。
- 没有 `stop` 就结束的流必须什么也不记录，也不保存 trace（[MMSP-RFC-0004](https://mmsp.penguin.ooo/zh/rfcs/mmsp-rfc-0004-message-fidelity/#61-%E5%8E%86%E5%8F%B2%E4%BF%9D%E5%AD%98%E5%89%AF%E6%9C%AC) §6.1、[MMSP-RFC-0010](https://mmsp.penguin.ooo/zh/rfcs/mmsp-rfc-0010-trace-recording/#3-%E4%BD%95%E6%97%B6%E4%BF%9D%E5%AD%98%E8%BD%A8%E8%BF%B9) §3）。

## 4. Python

每个 Python 客户端在服务商有流时，都必须在一个会关闭它的上下文中读取这条流（`async with stream:`；`google_genai` 用 `aclosing`，并且还会关闭它的请求打开的 HTTP 响应，因为它的 SDK 把这些响应留给垃圾回收），所以关闭客户端的生成器就会关闭连接。信号一被中止，基类就这样做：有读取在进行时，取消发起这次读取的任务，不管它是哪个任务；没有读取在进行时，就在客户端生成器停着的地方关闭它。`mmsp` 客户端用自己的 `streaming_response` 转发其服务端的流，它也会取消进行中的读取，但只在有读取进行时才关注信号，所以没有读取在进行时到来的中止，要到调用方下一次读取时才生效：这次读取抛出中止错误并关闭请求。`AutoLLMClient` 和 `streaming_response_stateful` 在自己的生成器关闭时，一并关闭它们读取的那个生成器。

## 5. TypeScript

每个 TypeScript 客户端都必须把信号交给它的 SDK，`mmsp` 客户端则交给 `fetch`。`google_genai` 通过自己的一个 `AbortController` 传递信号，它的生成器退出时也会中止这个控制器，因为关闭那个 SDK 的流并不会关闭连接。被中止的流必须抛出调用方的原因，而不是 SDK 的：SDK 悄悄结束它的流时，由基类抛出这个原因；`google_genai` 则用它替换 SDK 自己的 `AbortError` 重新抛出。

## 6. 有状态调用

中止必须立即释放有状态调用对客户端的占用，被中止的调用不得记录它的回合，即使中止恰好发生在 `stop` 到达之时（[MMSP-RFC-0004](https://mmsp.penguin.ooo/zh/rfcs/mmsp-rfc-0004-message-fidelity/#62-%E5%90%8C%E4%B8%80%E6%97%B6%E9%97%B4%E5%8F%AA%E6%9C%89%E4%B8%80%E6%AC%A1%E6%9C%89%E7%8A%B6%E6%80%81%E8%B0%83%E7%94%A8) §6.2）。恰好在服务商的流结束时发生的中止，也必须按中止结束这条流。

## 7. 服务端、playground 与 `mmsp` 客户端

- MMSP 服务端和 playground 发现调用方已离开时，必须中止它们的上游；playground 的 Stop 也必须中止上游。
- Python 服务端在写入失败时发现连接断开，也就是在下一个事件或下一次 keep-alive 时，从那一刻起立即关闭上游。
- `mmsp` 客户端的信号会在读取进行到一半时结束它的流；在两个事件之间，TypeScript 中立即结束，Python 中在调用方下一次读取时结束（§4）。

## 8. 不中止就离开

调用方在 `stop` 之前离开一条 Python 流，既不中止也不关闭它，就会一直占着发往服务商的请求，有状态调用还会占着客户端，直到生成器被关闭：要么调用 `await stream.aclose()`，要么等到再没有东西持有这个生成器时由事件循环关闭。在 TypeScript 中，离开 `for await` 就会关闭它。仍持有一条已离开的流的调用方，应当中止或关闭它。

## 9. Conformance

- `src_py/mmsp/base_client.py` / `src_ts/src/baseClient.ts`；`src_py/mmsp/abort_signal.py`；`src_py/mmsp/auto_client.py`；`src_py/mmsp/<client>/client.py` 和 `src_ts/src/<client>/client.ts` 中每个客户端的流循环。
- `src_py/mmsp/integration/server.py` 与 `playground.py`，以及 `src_ts/src/integration/server.ts` 与 `playground.ts` 中的对应实现。
- `src_py/tests/test_interruption.py` / `src_ts/tests/interruption.test.ts`，它们让每种流式客户端类型，分别以无状态和有状态的方式，从一个会陷入沉默的本地上游读取流，并检查：
  - 调用方等待时的中止、调用方忙时中止之后的下一次读取、`aclose()` / `return()`、离开循环，这些发生后 200 ms 内连接关闭；
  - 调用方拿到中止错误，没有 `.done`，也没有 `stop`；
  - 什么也不记录，也不保存 trace，中止发生在用量和结束原因到达之后也是如此。
- `test_abort_signal.py`；`test_mmsp_server.py` / `mmsp-server.test.ts`。
