Skip to content

Threads 9: Align Thread with AI SDK request semantics - #266

Merged
FranciscoMoretti merged 2 commits into
codex/threads-v2-08-external-statefrom
codex/threads-v2-09-ai-sdk-compatibility
Aug 3, 2026
Merged

Threads 9: Align Thread with AI SDK request semantics#266
FranciscoMoretti merged 2 commits into
codex/threads-v2-08-external-statefrom
codex/threads-v2-09-ai-sdk-compatibility

Conversation

@FranciscoMoretti

@FranciscoMoretti FranciscoMoretti commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Summary\n- align isolated threaded runs with AI SDK continuation, regeneration, reconnection, and restored tool ownership behavior\n- preserve tree topology while each run exposes a linear AI SDK request path\n- document the assistant-parent regeneration blocker in AI SDK\n\n## Verification\n- bun test packages/thread/test\n- bun run lint (packages/thread)\n- bun test:types\n\nPart of the Threads stack.

Summary by Sourcery

Align thread run lifecycle with AI SDK request semantics, including assistant continuation, regeneration, reconnection, and restored tool ownership behavior.

New Features:

  • Support continuing assistant messages via sendMessage without creating new child responses.
  • Allow regenerating assistant responses, including root assistants, while preserving tree topology and sibling order.
  • Enable resuming restored assistant-only trees and assistant messages whose parents may be assistants.

Bug Fixes:

  • Prevent regeneration of assistant messages whose parent is also an assistant to avoid unintended tree mutations under current AI SDK behavior.
  • Ensure tool calls and approvals regain a run owner after restoring a message tree so subsequent tool interactions work correctly.

Enhancements:

  • Refine run capacity checks and tracking to separate concurrency limiting from run creation, including for resumed and continued runs.
  • Introduce per-run linear message paths that can be truncated for regeneration without modifying the canonical message tree.
  • Adjust assistant run creation and selection so continuation, regeneration, and reconnection correctly reuse existing assistant nodes.

Build:

  • Change the types test script to run TypeScript type-checking with tsc before building.

Documentation:

  • Update architecture documentation to describe assistant continuation, regeneration semantics, reconnection behavior, and the current AI SDK regeneration blocker.

Tests:

  • Expand thread and AI SDK integration tests to cover assistant continuation, regeneration scenarios, reconnection and resume behavior, and restored tool ownership.
  • Add tests for resuming restored assistant-root trees and assistant-child trees whose parents are assistants.

Summary by cubic

Aligns Thread run behavior with AI SDK semantics: assistant continuations stay on the same message, regeneration creates sibling responses (including roots), and reconnection resumes the existing assistant node across restored trees. Preserves tree topology with a linear run-local request path and hardens resume/continuation and restored tool ownership.

  • New Features

    • sendMessage continues a selected or explicit assistant in-place; no child is created, overlapping continuations are rejected, and concurrency is enforced.
    • regenerate({ messageId }) uses AI SDK’s native trigger, truncates only the run-local path, inserts a sibling replacement (supports roots), and only follows when still on the target.
    • Resume/reconnect reactivates the existing assistant node (including restored roots and assistant-with-assistant-parent) with global concurrency checks; onFinish receives the completed run path even after navigation.
    • Restores and validates tool call/approval ownership after restore, enabling addToolApprovalResponse and addToolOutput; errors on missing or duplicate owners.
  • Refactors

    • Introduced initialPathMessageId and run-local path management; host now provides getMessagePath.
    • Registry enforces capacity with assertHasCapacity and can find runs for approvals/tool calls and response messages.
    • Blocks regeneration when an assistant’s parent is an assistant to avoid AI SDK miscontinuation.
    • ThreadRunChat adds startWithMessage, regenerateMessage, and refreshPath; aligns sendMessages ID handling and onFinish with the message path.
    • Updated docs; test:types runs tsc before build.

Written for commit f8e22ed. Summary will update on new commits.

Review in cubic

Stack

  1. Threads 8: Add externally owned thread state #262
  2. Threads 9: Align Thread with AI SDK request semantics #266 👈 current
  3. Threads 10: Back threads with canonical Zustand state #267
  4. Threads 11: Mount useThread in ChatJS #241
  5. Threads 12: Add branch navigation and retry #242
  6. Threads 13: Isolate branch stream lifecycles #243
  7. Threads 14: Stream follow-up parallel responses #244
  8. Threads 15: Add cancellable request gates #263
  9. Threads 16: Gate first-message parallel runs #245
  10. Threads 17: Stop newly provisioned thread runs #246
  11. Threads 18: Show parallel response lifecycle states #247
  12. Threads 19: Publish installable thread sources #248
  13. Threads 20: Add the thread playground model #249
  14. Threads 21: Add the interactive thread playground #250
  15. Threads 22: Publish the threads product page #251
  16. Threads 23: Add the value-first package guide #252
  17. Threads 24: Document ChatJS threaded behavior #253
  18. Threads 25: Prepare the thread package release #254

@cursor

cursor Bot commented Aug 2, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@sourcery-ai

sourcery-ai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Reviewer's Guide

Aligns Thread’s run lifecycle and tree behavior with AI SDK semantics by treating assistant messages as continuations rather than parents of new runs, tracking a run-local linear path, reconstructing tool ownership on restore, and tightening concurrency and regeneration rules, with tests and docs updated accordingly.

Sequence diagram for assistant continuation vs new run in sendMessage

sequenceDiagram
  actor User
  participant AbstractThread
  participant RunRegistry
  participant ThreadRunChat

  User->>AbstractThread: sendMessage(input, options?)
  alt input is empty and cursor is assistant
    AbstractThread->>AbstractThread: continueAssistant({ follow, messageId, options })
    AbstractThread->>RunRegistry: createRunForAssistant(messageId)
    RunRegistry-->>AbstractThread: RunRecord
    AbstractThread->>RunRegistry: assertHasCapacity(run.spec.parentMessageId)
    AbstractThread->>ThreadRunChat: run.chat.start(options)
    ThreadRunChat-->>AbstractThread: finished
  else normal new run
    AbstractThread->>RunRegistry: assertHasCapacity(parentMessageId)
    AbstractThread->>AbstractThread: startRun({ follow, from, input, options })
    AbstractThread->>ThreadRunChat: startRunRequest(spec, chat.start)
    ThreadRunChat-->>AbstractThread: finished
  end
Loading

File-Level Changes

Change Details Files
Align run lifecycle and assistant-message semantics with AI SDK (continuation, regeneration, resume).
  • Replace generic run-path lookup with message-based paths using initialPathMessageId to give each run a local linear AI SDK request path while preserving the canonical tree.
  • Introduce ThreadRunState-local message arrays instead of reading paths from the tree on every access, and ensure onFinish uses the canonical tree path for the final messages.
  • Change ThreadRunChat.start/startWithMessage/regenerateMessage wiring so submit, continuation, and regeneration use the appropriate AI SDK triggers and messageId behavior (e.g., no synthetic response IDs).
  • Update concurrency checks from assertCanStart to assertHasCapacity and ensure resumeStream enforces global concurrency limits without revalidating parent roles.
  • Allow reconnection and resume of restored assistant nodes (including assistant-with-assistant-parent) while still respecting AI SDK run capacity.
packages/thread/src/ai-sdk-run-chat.ts
packages/thread/src/abstract-thread.ts
packages/thread/src/run-registry.ts
packages/thread/test/ai-sdk-run-chat.test.ts
packages/thread/test/thread.test.ts
Refine assistant message generation, continuation, and regeneration behavior in Thread.
  • Disallow creating independent runs from assistant parents by replacing assertCanStartRun with assertCanGenerateFrom, but allow assistant continuation via sendMessage when called with or on an assistant.
  • Add continueAssistant and startAssistantMessage helpers to continue existing assistant nodes or start runs tied to explicit assistant messages without creating new child nodes.
  • Ensure regeneration uses AI SDK’s regenerate-message trigger, preserves tree topology by inserting a sibling replacement at the reserved sibling order, and only auto-follows the new response if the cursor still points at the target.
  • Reject regeneration of an assistant whose parent is an assistant, documenting this as a temporary AI SDK regeneration bug workaround.
  • Adjust cursor movement and run selection to distinguish between following the active branch and preserving user navigation when regeneration or streaming completes.
packages/thread/src/abstract-thread.ts
packages/thread/src/ai-sdk-run-chat.ts
packages/thread/test/thread.test.ts
packages/thread/ARCHITECTURE.md
Rebuild tool-call and approval ownership for restored trees and improve run registry lookups.
  • Introduce findAssistantOwningPart to scan assistant messages for tool/approval parts and ensure a single owning assistant per toolCallId/approvalId.
  • Add getOrCreateRunForApproval and getOrCreateRunForToolCall, which either reuse existing runs or create new assistant-owned runs when handling tool approvals/outputs after a restore.
  • Extend RunRegistry with findForApproval, findForToolCall, and getForResponseMessage to support non-throwing lookups and mapping responses back to runs.
  • Update addToolApprovalResponse/addToolOutput to use the new lazy run creation so restored threads can properly route tool events.
packages/thread/src/abstract-thread.ts
packages/thread/src/run-registry.ts
packages/thread/test/thread.test.ts
Clarify and extend architecture docs and tests to cover new semantics.
  • Update ARCHITECTURE.md to describe AbstractThread’s role, run-local linear paths, assistant-continuation semantics, regeneration behavior, and the AI SDK regeneration blocker.
  • Add tests for assistant continuation (explicit assistant sendMessage and selected-assistant continuation) ensuring no extra sibling responses are created.
  • Add tests for regeneration behavior including root assistants, non-following cursor when navigating away, and rejection when the parent is an assistant.
  • Add tests verifying tool/approval ownership reconstruction after restore and resuming restored assistant nodes (root and with assistant parents).
  • Adjust ControlledTransport and test harnesses to capture full send options (trigger, messageId, messages) and use tsc-based type tests instead of build-only for test:types.
packages/thread/ARCHITECTURE.md
packages/thread/test/thread.test.ts
packages/thread/test/ai-sdk-run-chat.test.ts
packages/thread/package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-js-docs Ready Ready Preview Aug 2, 2026 8:46am
chat-js-site Ready Ready Preview Aug 2, 2026 8:46am
sparka Ready Ready Preview Aug 2, 2026 8:46am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f9e3ebeb-da55-44cc-a36c-3bef236dba01

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The new findAssistantOwningPart helpers scan the entire tree on each tool approval/output when a run is missing; consider indexing tool/approval IDs on restore (similar to RunRegistry maps) to avoid repeated full-tree scans on larger threads.
  • With ThreadRunState now caching #messages instead of reading from the host each time, please double-check that tree mutations outside the run (e.g. other runs or manual edits) cannot desynchronize this cache from the canonical tree; if they can, it may be safer to derive the path from the tree when needed or explicitly document the invariants.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `findAssistantOwningPart` helpers scan the entire tree on each tool approval/output when a run is missing; consider indexing tool/approval IDs on restore (similar to `RunRegistry` maps) to avoid repeated full-tree scans on larger threads.
- With `ThreadRunState` now caching `#messages` instead of reading from the host each time, please double-check that tree mutations outside the run (e.g. other runs or manual edits) cannot desynchronize this cache from the canonical tree; if they can, it may be safer to derive the path from the tree when needed or explicitly document the invariants.

## Individual Comments

### Comment 1
<location path="packages/thread/test/thread.test.ts" line_range="728-737" />
<code_context>
 		);
 	});

+	test("reconstructs tool and approval ownership after restoring a tree", async () => {
+		const source = new Thread();
+		source.addMessage(user("user-1"), null);
+		source.addMessage(
+			{
+				id: "assistant-1",
+				parts: [
+					{
+						approval: { id: "approval-1" },
+						input: { value: 1 },
+						state: "approval-requested",
+						toolCallId: "tool-1",
+						toolName: "test-tool",
+						type: "dynamic-tool",
+					},
+				],
+				role: "assistant",
+			},
+			"user-1",
+		);
+		source.setCursor("assistant-1");
+		const restored = new Thread();
+		restored.restore(source.getTreeSnapshot());
+
+		await restored.addToolApprovalResponse({
+			approved: true,
+			id: "approval-1",
+		});
+		await restored.addToolOutput({
+			output: "restored output",
+			tool: "test-tool",
+			toolCallId: "tool-1",
+		});
+
+		expect(restored.getMessage("assistant-1")?.parts).toContainEqual(
+			expect.objectContaining({
+				approval: expect.objectContaining({
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for error paths in restored tool/approval ownership (missing or duplicate ownership).

To fully cover the new ownership reconstruction logic, please also add tests for:

1. A restored tree where the approval/toolCall id is missing from all assistant messages, asserting that `addToolApprovalResponse`/`addToolOutput` throw the expected error.
2. A restored tree where the same approval/toolCall id appears in multiple assistant messages, asserting that the duplicate-owner case throws the configured error.

These will exercise the new guardrails and help catch future regressions in error handling around ownership reconstruction.
</issue_to_address>

### Comment 2
<location path="packages/thread/test/ai-sdk-run-chat.test.ts" line_range="76-79" />
<code_context>

-	getRunPath = () =>
-		this.tree.getPath(this.spec.messageId ?? this.spec.parentMessageId);
+	getMessagePath = (messageId: string | null) => this.tree.getPath(messageId);
 	updateRunPath = (messages: UIMessage[]) => {
 		this.tree.updatePath(messages);
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for the new `initialPathMessageId`/`messages` tracking and `messageId` propagation in `ThreadRunChat`.

The new host/spec behavior (`getMessagePath`, `initialPathMessageId`) and the internal `#messages` array in `ThreadRunState` are not yet covered by tests. I’d suggest adding:

1. A `ThreadRunState` test that initializes `messages` from `getMessagePath(initialPathMessageId)` and verifies `pushMessage`, `popMessage`, and `replaceMessage` correctly update the internal array, including the guard that only allows replacing the current response.
2. A `sendMessages` test that confirms `messageId` is omitted only when `spec.messageId` is `undefined` and `trigger === "submit-message"`, and preserved for other triggers (e.g. `regenerate-message`) or when `spec.messageId` is set.
3. An `onFinish` test that checks it calls `host.getMessagePath(spec.messageId ?? spec.parentMessageId)` and returns the correct linear path for both explicit `messageId` and parent-only runs.

These will ensure the updated request/path semantics are validated at the run chat layer and guard against regressions in `messageId` and path handling.

Suggested implementation:

```typescript
		this.tree = new MessageTree({ messages: [userMessage] });
	}

	getMessagePath = (messageId: string | null) => this.tree.getPath(messageId);
	updateRunPath = (messages: UIMessage[]) => {
		this.tree.updatePath(messages);
	};

function createSpec(overrides: Partial<ThreadRunSpec> = {}): ThreadRunSpec {
	return {
		id: "run-1",
		initialPathMessageId: "user-1",
		parentMessageId: "user-1",
		siblingOrder: 0,
		...overrides,
	};
}

describe("ThreadRunState message path tracking", () => {
	it("initializes internal messages from getMessagePath(initialPathMessageId)", () => {
		const userMessage: UIMessage = {
			id: "user-1",
			role: "user",
			content: [{ type: "text", text: "Hello" }],
		};

		const host = new (class {
			tree: MessageTree<UIMessage>;
			constructor() {
				this.tree = new MessageTree({ messages: [userMessage] });
			}
			getMessagePath = (messageId: string | null) =>
				this.tree.getPath(messageId);
			updateRunPath = (messages: UIMessage[]) => {
				this.tree.updatePath(messages);
			};
		})();

		const spec = createSpec({ initialPathMessageId: "user-1" });
		const state = new ThreadRunState({ host, spec });

		// The internal #messages array should be initialized from getMessagePath(initialPathMessageId)
		const initialPath = host.getMessagePath(spec.initialPathMessageId);
		expect(state.messages).toEqual(initialPath);
	});

	it("pushMessage appends the new message and updates host path", () => {
		const userMessage: UIMessage = {
			id: "user-1",
			role: "user",
			content: [{ type: "text", text: "Hello" }],
		};

		const host = new (class {
			tree: MessageTree<UIMessage>;
			constructor() {
				this.tree = new MessageTree({ messages: [userMessage] });
			}
			getMessagePath = (messageId: string | null) =>
				this.tree.getPath(messageId);
			updateRunPath = jest.fn((messages: UIMessage[]) => {
				this.tree.updatePath(messages);
			});
		})();

		const spec = createSpec({ initialPathMessageId: "user-1" });
		const state = new ThreadRunState({ host, spec });

		const responseMessage: UIMessage = {
			id: "assistant-1",
			role: "assistant",
			content: [{ type: "text", text: "World" }],
		};

		state.pushMessage(responseMessage);

		expect(state.messages[state.messages.length - 1]).toEqual(responseMessage);
		expect(host.updateRunPath).toHaveBeenCalledWith(state.messages);
	});

	it("popMessage removes the last message and updates host path", () => {
		const userMessage: UIMessage = {
			id: "user-1",
			role: "user",
			content: [{ type: "text", text: "Hello" }],
		};
		const responseMessage: UIMessage = {
			id: "assistant-1",
			role: "assistant",
			content: [{ type: "text", text: "World" }],
		};

		const host = new (class {
			tree: MessageTree<UIMessage>;
			constructor() {
				this.tree = new MessageTree({ messages: [userMessage, responseMessage] });
			}
			getMessagePath = (messageId: string | null) =>
				this.tree.getPath(messageId);
			updateRunPath = jest.fn((messages: UIMessage[]) => {
				this.tree.updatePath(messages);
			});
		})();

		const spec = createSpec({ initialPathMessageId: "assistant-1" });
		const state = new ThreadRunState({ host, spec });

		// sanity check: we start with both messages
		expect(state.messages.map((m) => m.id)).toEqual(["user-1", "assistant-1"]);

		state.popMessage();

		expect(state.messages.map((m) => m.id)).toEqual(["user-1"]);
		expect(host.updateRunPath).toHaveBeenCalledWith(state.messages);
	});

	it("replaceMessage only replaces the current response message", () => {
		const userMessage: UIMessage = {
			id: "user-1",
			role: "user",
			content: [{ type: "text", text: "Hello" }],
		};
		const responseMessage: UIMessage = {
			id: "assistant-1",
			role: "assistant",
			content: [{ type: "text", text: "World" }],
		};

		const host = new (class {
			tree: MessageTree<UIMessage>;
			constructor() {
				this.tree = new MessageTree({ messages: [userMessage, responseMessage] });
			}
			getMessagePath = (messageId: string | null) =>
				this.tree.getPath(messageId);
			updateRunPath = jest.fn((messages: UIMessage[]) => {
				this.tree.updatePath(messages);
			});
		})();

		const spec = createSpec({ initialPathMessageId: "assistant-1" });
		const state = new ThreadRunState({ host, spec });

		const newResponse: UIMessage = {
			id: "assistant-2",
			role: "assistant",
			content: [{ type: "text", text: "New World" }],
		};

		// Replacing the current response should succeed
		state.replaceMessage(newResponse);
		expect(state.messages.map((m) => m.id)).toEqual(["user-1", "assistant-2"]);
		expect(host.updateRunPath).toHaveBeenCalledWith(state.messages);

		// Attempting to replace a non-current response should be ignored/guarded
		const invalidReplacement: UIMessage = {
			id: "user-2",
			role: "user",
			content: [{ type: "text", text: "Should not replace" }],
		};

		state.replaceMessage(invalidReplacement);
		// The assistant response should still be the last message
		expect(state.messages.map((m) => m.id)).toEqual(["user-1", "assistant-2"]);
	});
});

describe("ThreadRunChat sendMessages messageId propagation", () => {
	it("omits messageId only when spec.messageId is undefined and trigger === 'submit-message'", async () => {
		const host = {
			getMessagePath: jest.fn(),
			updateRunPath: jest.fn(),
		};

		const specWithoutMessageId = createSpec({ parentMessageId: "user-1", initialPathMessageId: "user-1" });
		const specWithMessageId = createSpec({
			messageId: "user-2",
			parentMessageId: "user-1",
			initialPathMessageId: "user-2",
		});

		const threadRunWithoutMessageId = new ThreadRunChat({ host, spec: specWithoutMessageId });
		const threadRunWithMessageId = new ThreadRunChat({ host, spec: specWithMessageId });

		const messages: UIMessage[] = [
			{
				id: "user-1",
				role: "user",
				content: [{ type: "text", text: "Hello" }],
			},
		];

		// submit-message trigger with undefined spec.messageId should omit messageId
		const submitResult = await threadRunWithoutMessageId.sendMessages(messages, {
			trigger: "submit-message",
		});

		expect(submitResult.request.messages[0].id).toBeUndefined();

		// regenerate-message trigger should preserve messageId even when spec.messageId is undefined
		const regenerateResult = await threadRunWithoutMessageId.sendMessages(messages, {
			trigger: "regenerate-message",
		});

		expect(regenerateResult.request.messages[0].id).toBe("user-1");

		// when spec.messageId is set, messageId should always be preserved
		const withMessageIdResult = await threadRunWithMessageId.sendMessages(messages, {
			trigger: "submit-message",
		});

		expect(withMessageIdResult.request.messages[0].id).toBe("user-1");
	});

	it("preserves messageId for non-submit triggers", async () => {
		const host = {
			getMessagePath: jest.fn(),
			updateRunPath: jest.fn(),
		};

		const spec = createSpec({ parentMessageId: "user-1", initialPathMessageId: "user-1" });
		const threadRun = new ThreadRunChat({ host, spec });

		const messages: UIMessage[] = [
			{
				id: "user-1",
				role: "user",
				content: [{ type: "text", text: "Hello" }],
			},
		];

		const result = await threadRun.sendMessages(messages, {
			trigger: "regenerate-message",
		});

		expect(result.request.messages[0].id).toBe("user-1");
	});
});

describe("ThreadRunChat onFinish path resolution", () => {
	it("calls host.getMessagePath(spec.messageId ?? spec.parentMessageId) with explicit messageId", async () => {
		const userMessage: UIMessage = {
			id: "user-1",
			role: "user",
			content: [{ type: "text", text: "Hello" }],
		};
		const responseMessage: UIMessage = {
			id: "assistant-1",
			role: "assistant",
			content: [{ type: "text", text: "World" }],
		};

		const host = {
			tree: new MessageTree<UIMessage>({ messages: [userMessage, responseMessage] }),
			getMessagePath: jest.fn(function (this: any, messageId: string | null) {
				return this.tree.getPath(messageId);
			}),
			updateRunPath: jest.fn(),
		};

		const spec = createSpec({
			messageId: "assistant-1",
			parentMessageId: "user-1",
			initialPathMessageId: "assistant-1",
		});

		const threadRun = new ThreadRunChat({ host, spec });

		const resultPath = await threadRun.onFinish();

		expect(host.getMessagePath).toHaveBeenCalledWith("assistant-1");
		expect(resultPath.map((m) => m.id)).toEqual(["user-1", "assistant-1"]);
	});

	it("calls host.getMessagePath(spec.messageId ?? spec.parentMessageId) with parent-only runs", async () => {
		const userMessage: UIMessage = {
			id: "user-1",
			role: "user",
			content: [{ type: "text", text: "Hello" }],
		};

		const host = {
			tree: new MessageTree<UIMessage>({ messages: [userMessage] }),
			getMessagePath: jest.fn(function (this: any, messageId: string | null) {
				return this.tree.getPath(messageId);
			}),
			updateRunPath: jest.fn(),
		};

		const spec = createSpec({
			messageId: undefined,
			parentMessageId: "user-1",
			initialPathMessageId: "user-1",
		});

		const threadRun = new ThreadRunChat({ host, spec });

		const resultPath = await threadRun.onFinish();

		expect(host.getMessagePath).toHaveBeenCalledWith("user-1");
		expect(resultPath.map((m) => m.id)).toEqual(["user-1"]);
	});
});

```

To fully integrate these tests, the following adjustments will likely be needed elsewhere in `ai-sdk-run-chat.test.ts`:

1. **Imports**:  
   - Ensure `ThreadRunState`, `ThreadRunChat`, `MessageTree`, and `UIMessage` are imported from their respective modules. If the file already imports them, avoid duplicate imports; otherwise add:
     - `import { ThreadRunState } from "../src/ThreadRunState";` (or the correct path in your repo)
     - `import { ThreadRunChat } from "../src/ThreadRunChat";`
     - `import { MessageTree } from "../src/MessageTree";`
     - `import type { UIMessage } from "../src/types";`
   - Make sure `jest` globals (`jest.fn`, `expect`, `describe`, `it`) are available; if using `vitest`, replace `jest.fn` with `vi.fn` and adjust imports accordingly.

2. **Internal API alignment**:  
   - The tests assume `ThreadRunState` exposes an internal `messages` array and methods `pushMessage`, `popMessage`, and `replaceMessage`. If these are private or named differently, either:
     - Expose them for testing (e.g. via a getter), or
     - Update the tests to use the public API that drives those methods.
   - The `sendMessages` tests assume `ThreadRunChat.sendMessages` returns an object with `request.messages`. If your actual API differs, adjust the assertions to match the real shape.

3. **Trigger semantics**:  
   - If your `trigger` type is an enum or union with different string values than `"submit-message"` / `"regenerate-message"`, update the triggers in the tests to the correct values.

4. **Host mock shape**:  
   - Align the `host` mock objects with the actual `ThreadRunChat`/`ThreadRunState` constructor expectations (e.g. if the host is a class instance rather than a plain object, use the same pattern as existing tests in the file).

5. **createSpec helper**:  
   - If `ThreadRunSpec` includes additional required properties beyond `id`, `initialPathMessageId`, `parentMessageId`, and `siblingOrder`, extend the `createSpec` helper to include defaults for them so the tests construct valid specs.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread packages/thread/test/thread.test.ts
Comment thread packages/thread/test/ai-sdk-run-chat.test.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 7 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/thread/src/ai-sdk-run-chat.ts
Comment thread packages/thread/src/abstract-thread.ts Outdated
@FranciscoMoretti
FranciscoMoretti merged commit 5abab0f into main Aug 3, 2026
15 checks passed
@FranciscoMoretti
FranciscoMoretti deleted the codex/threads-v2-09-ai-sdk-compatibility branch August 3, 2026 07:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant