Skip to content

Commit 9e196c9

Browse files
feat(thread): finalize extensible AI SDK runtime
1 parent 0e3402e commit 9e196c9

7 files changed

Lines changed: 526 additions & 80 deletions

File tree

packages/thread/ARCHITECTURE.md

Lines changed: 49 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ message ID remains unknown until AI SDK first publishes the response and may
280280
then come from the server. Message-based helpers resolve the run associated with
281281
that node after this binding occurs.
282282

283-
`Thread` creates a `ThreadRunChat` when `sendMessage`, `startRun`, or a
283+
`AbstractThread` creates a `ThreadRunChat` when `sendMessage`, `startRun`, or a
284284
reconnection needs an AI SDK request lifecycle. Completed run records are
285285
currently retained so their status and ownership information remain
286286
addressable.
@@ -291,16 +291,27 @@ run has independent status, error, request serialization, and cancellation.
291291
Automatic tool continuations remain in the same run and update the same
292292
assistant response node.
293293

294-
A new live run requires a non-assistant response parent. AI SDK interprets an
295-
assistant message at the end of a request as the response to continue, not as
296-
the parent of another response. Therefore, a bare `startRun` from an assistant
297-
message is rejected. Branching from an assistant remains supported by attaching
298-
a new input message with `sendMessage` and generating from that input.
294+
A new independent response requires a non-assistant response parent. AI SDK
295+
interprets an assistant message at the end of a request as the response to
296+
continue, not as the parent of another response. Therefore, a bare `startRun`
297+
from an assistant message is rejected.
298+
299+
The `useChat`-compatible `sendMessage` surface keeps AI SDK's continuation
300+
semantics. Calling `sendMessage()` on a selected assistant continues that node,
301+
and passing an explicit assistant message attaches and streams into that same
302+
message ID. Neither operation creates a child response under the assistant.
303+
Applications create a branch below an assistant by attaching a new input
304+
message and generating from that input.
299305

300306
This is a `Thread` live-run invariant, not a `MessageTree` invariant. The
301307
tree remains role-agnostic so persisted, restored, or server-created data may
302308
contain assistant-to-assistant edges.
303309

310+
Reconnection does not create response topology. It reactivates the existing
311+
assistant node, so its parent may have any role or be `null`. Resume checks
312+
concurrency capacity but does not apply the new-response parent-role rule,
313+
matching AI SDK's `resumeStream` behavior over the current message history.
314+
304315
## AI SDK Integration
305316

306317
`ThreadRunChat` extends AI SDK's `AbstractChat` for one run. It reuses AI SDK
@@ -315,9 +326,10 @@ behavior for:
315326
- `sendAutomaticallyWhen`
316327
- regeneration and reconnection
317328

318-
The internal `ThreadRunState` presents one linear branch path to
319-
`AbstractChat`. It writes accumulated assistant snapshots into `Thread`
320-
when AI SDK publishes the streaming response.
329+
The internal `ThreadRunState` presents one run-local linear branch path to
330+
`AbstractChat`. AI SDK may truncate that local path for regeneration without
331+
deleting nodes from the canonical tree. The state writes accumulated assistant
332+
snapshots into `Thread` when AI SDK publishes the streaming response.
321333

322334
`ThreadRunChat` does not insert a synthetic response into that path. AI SDK
323335
creates the provisional assistant response using its normal last-message rules
@@ -369,6 +381,34 @@ As in AI SDK, expected transport and stream failures resolve after publishing
369381
`AbstractChat` reject `sendMessage`, `resumeRun`, and the corresponding
370382
`finished` promise.
371383

384+
## Regeneration
385+
386+
`regenerate({ messageId })` invokes `AbstractChat.regenerate` inside an isolated
387+
run adapter. The transport therefore receives AI SDK's native
388+
`regenerate-message` trigger and target `messageId`.
389+
390+
AI SDK truncates only the adapter's linear request path. The canonical tree
391+
retains the target response, then inserts the streamed replacement beside it
392+
using the run's reserved sibling order. A root assistant regenerates into
393+
another root. The cursor follows the replacement on its first write only when
394+
it still points to the regeneration target, so navigation during submission or
395+
streaming is not overwritten.
396+
397+
### Known AI SDK Blocker
398+
399+
Regenerating an assistant whose parent is another assistant remains rejected.
400+
This is blocked by an AI SDK regeneration bug: after truncating the path to the
401+
assistant parent, AI SDK treats that trailing assistant as the response to
402+
continue. Regenerating `[user, assistant A, assistant B]` therefore produces
403+
`[user, assistant A']` instead of creating a replacement sibling for
404+
`assistant B`.
405+
406+
The intended tree result is to preserve `assistant B` and create another child
407+
of `assistant A`. Once AI SDK distinguishes regeneration from normal trailing
408+
assistant continuation, `Thread` can remove this rejection and keep using the
409+
native regeneration lifecycle. Until then, rejecting the operation avoids
410+
silently mutating a shared ancestor.
411+
372412
## Status and Cancellation
373413

374414
Runs use the AI SDK `ChatStatus` values:

packages/thread/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"prepublishOnly": "bun run build",
5252
"test": "bun test --pass-with-no-tests",
5353
"test:unit": "bun test --pass-with-no-tests",
54-
"test:types": "bun run build"
54+
"test:types": "tsc -p tsconfig.json --noEmit && bun run build"
5555
},
5656
"peerDependencies": {
5757
"@ai-sdk/react": "^3.0.0",

0 commit comments

Comments
 (0)