Skip to content

agentLoop-text split#175

Closed
jherr wants to merge 38 commits intomainfrom
feat/experimental-agentLoop
Closed

agentLoop-text split#175
jherr wants to merge 38 commits intomainfrom
feat/experimental-agentLoop

Conversation

@jherr
Copy link
Contributor

@jherr jherr commented Dec 22, 2025

🎯 Changes

Creates a text activity and an agentLoop function to replace the agentic loop functionality and text generation in chat.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced experimental text() function for straightforward text generation with streaming and structured output support.
    • Introduced experimental agentLoop() function enabling agentic workflows with automatic tool execution, approval flows, and configurable loop strategies.
  • Bug Fixes & Improvements

    • Refactored chat API to delegate to new text/agent abstractions for improved consistency.
  • Documentation

    • Added migration guide from deprecated chat() to new text() and agentLoop() APIs.
    • Updated API reference with new functions and types.
  • Breaking Changes

    • Removed ThinkingStreamChunk from public API type union.

AlemTuzlak and others added 30 commits December 10, 2025 15:17
* fix: refactoring ai for more activities

* smoke tests passing

* woot, all the test stuff is working

* dev panel updates for images, summarization, one shot and structured

* enhancing smoke tests

* fixing tests

* adding grok

* last minute tests

* Refactor imports in documentation and examples to use named imports for `ai`

- Updated all instances of `import ai from "@tanstack/ai"` to `import { ai } from "@tanstack/ai"` across various documentation files, guides, and examples.
- Ensured consistency in import statements for better clarity and adherence to best practices.

* ci: apply automated fixes

* fix typesafety on ai

* ci: apply automated fixes

* cleanup types

* ci: apply automated fixes

* remove grok

* ci: apply automated fixes

* fix provenence?

* update deps

* fix tests

---------

Co-authored-by: Alem Tuzlak <t.zlak@hotmail.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* video generation

* text to speech and speech to text

* adding some cool audio UI to the dev panel

* small fixups

* ci: apply automated fixes

* client fixes on tool calls

* few more client fixups

* one last test fix

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
- Add createOptions() function for type-safe adapter option creation
- Refactor OpenAI summarize adapter to use text adapter for streaming
- Deprecate textOptions() in favor of createOptions()
- Update examples to use createOptions pattern
- Add runtime adapter switching documentation guide
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Dec 22, 2025

📝 Walkthrough

Walkthrough

Introduces experimental text() API for one-shot text generation and agentLoop() for agentic workflows with tool execution. Refactors chat() activity to delegate to these new components. Updates documentation and public API exports, removes deprecated ThinkingStreamChunk from public types, and adds comprehensive type-safety tests.

Changes

Cohort / File(s) Summary
Changeset & Documentation
.changeset/two-bikes-kneel.md, docs/api/ai.md, docs/reference/index.md
Added changeset entry for experimental text/agentLoop features. Updated API documentation with migration guide from chat to text/agentLoop, deprecated chat examples, and updated StreamChunk union (removed ThinkingStreamChunk). Removed chat function entry from reference index.
API Reference Documentation
docs/reference/classes/BaseAdapter.md, docs/reference/interfaces/AIAdapter.md
Added comprehensive documentation for BaseAdapter class and AIAdapter interface, detailing generic type parameters, public properties, methods, and type-level contracts.
Function & Type Reference
docs/reference/functions/messages.md, docs/reference/functions/textOptions.md, docs/reference/type-aliases/TextStreamOptionsForModel.md, docs/reference/type-aliases/TextStreamOptionsUnion.md
Added documentation for messages helper, textOptions function, and type aliases for TextStreamOptionsForModel and TextStreamOptionsUnion, detailing type constraints and usage patterns.
Text Activity Implementation
packages/typescript/ai/src/activities/text/index.ts, packages/typescript/ai/src/activities/index.ts
Implemented new standalone text activity with streaming, non-streaming, and structured output modes. Exports text, createTextOptions, and TextOptions/TextResult types as experimental APIs in activities index.
Agent Loop Implementation
packages/typescript/ai/src/agent/index.ts
Implemented comprehensive agentLoop runtime with tool execution orchestration, streaming and structured output support, event emission, and both direct-options and textFn-based API variants. Includes internal AgentLoopEngine and helper utilities.
Chat Activity Refactoring
packages/typescript/ai/src/activities/chat/index.ts
Substantially refactored chat to delegate to text() for non-tool workflows and agentLoop() for tool-based workflows. Removed internal TextEngine and tool orchestration; now routes through shared text/agent abstractions.
Public API Exports
packages/typescript/ai/src/index.ts
Added experimental exports: experimental_text, experimental_createTextOptions, ExperimentalTextOptions, ExperimentalTextResult, experimental_agentLoop, and AgentLoop\*-related types without removing existing exports.
Type Safety Tests
packages/typescript/ai/tests/generate-types.test-d.ts
Added comprehensive TypeScript type-safety test suite validating return types, model validity, providerOptions strictness, outputSchema compatibility, streaming toggles, and multimodal input handling across text/embedding/summarize/image adapters.
Smoke Tests
packages/typescript/smoke-tests/adapters/src/tests/emb-embedding.ts
Added new embedding smoke test module that validates embedding results, vector lengths, and usage data; integrates with test harness for pass/fail reporting.
Helper Functions
testing/panel/tests/helpers.ts
Introduced generic withRetry helper for rate-limited API calls with exponential backoff. Wrapped summarize API call paths to automatically retry on transient failures (429/RESOURCE_EXHAUSTED errors).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant text as text() Activity
    participant StreamProcessor as Streaming<br/>Processor
    participant Adapter
    participant DevTools
    
    Client->>text: text({adapter, messages, tools, stream: true})
    text->>text: create request ID
    text->>DevTools: emit text:request
    
    text->>StreamProcessor: runStreamingText()
    StreamProcessor->>Adapter: adapter.chatStream(options)
    
    loop for each StreamChunk
        Adapter-->>StreamProcessor: StreamChunk
        StreamProcessor->>DevTools: emit text:chunk
        StreamProcessor-->>Client: yield StreamChunk
    end
    
    StreamProcessor->>DevTools: emit text:finished / text:completed
    StreamProcessor-->>Client: stream complete
Loading
sequenceDiagram
    participant Client
    participant agentLoop as agentLoop()
    participant Engine as AgentLoopEngine
    participant textFn as text() Function
    participant Adapter
    participant ToolExecutor as Tool Executor
    
    Client->>agentLoop: agentLoop({textCreator, tools, maxIterations})
    
    loop while not finished
        agentLoop->>Engine: run cycle
        Engine->>textFn: call text() with messages
        textFn->>Adapter: adapter.chatStream()
        Adapter-->>textFn: StreamChunk (with tool calls)
        textFn-->>Engine: return message with tools
        
        Engine->>Engine: extract tool calls
        alt Tools require approval
            Engine->>Client: emit approval:request
            Client-->>Engine: approval response
        end
        
        Engine->>ToolExecutor: execute approved tools
        ToolExecutor-->>Engine: tool results
        
        Engine->>Engine: append tool results to messages
        Engine->>Client: emit tool:complete
    end
    
    Engine-->>Client: final message / structured result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 A hop through text flows, both swift and deep,
AgentLoops that circle and leap,
Tools at the ready, approvals in place,
One message at a time, at a measured pace!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'agentLoop-text split' directly reflects the main change: splitting chat into two new activities (text and agentLoop), which is the primary objective of this PR.
Description check ✅ Passed The PR description follows the template structure with all required sections completed: Changes (with context), Checklist (both items completed), and Release Impact (changeset generated as required).
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/experimental-agentLoop

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 and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Dec 22, 2025

View your CI Pipeline Execution ↗ for commit af72d51

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 7s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-19 18:23:00 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Dec 22, 2025

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@175

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@175

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@175

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@175

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@175

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@175

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@175

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@175

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@175

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@175

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@175

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@175

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@175

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@175

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@175

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@175

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@175

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@175

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@175

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@175

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@175

commit: af72d51

@jherr jherr changed the title [WIP] Feat: experimental agent loop agentLoop-text split Feb 19, 2026
@jherr jherr marked this pull request as ready for review February 19, 2026 18:30
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
docs/api/ai.md (1)

396-415: ⚠️ Potential issue | 🟠 Major

Update StreamChunk documentation to reflect AG-UI protocol implementation.

The documented StreamChunk type definition is stale. The actual implementation defines StreamChunk = AGUIEvent, which is a union of AG-UI protocol event types (RunStartedEvent, RunFinishedEvent, RunErrorEvent, TextMessageStartEvent, TextMessageContentEvent, TextMessageEndEvent, ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, StepStartedEvent, etc.), not the ContentStreamChunk, ThinkingStreamChunk, ToolCallStreamChunk, ToolResultStreamChunk, DoneStreamChunk, and ErrorStreamChunk shown in the current doc snippet. The documentation needs to be rewritten to match the current AG-UI event-based architecture.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/api/ai.md` around lines 396 - 415, The docs currently list StreamChunk
as a union of legacy types (ContentStreamChunk, ThinkingStreamChunk,
ToolCallStreamChunk, ToolResultStreamChunk, DoneStreamChunk, ErrorStreamChunk);
update the documentation to reflect the current implementation where StreamChunk
= AGUIEvent and describe the AG-UI protocol event types instead — mention
concrete event names like RunStartedEvent, RunFinishedEvent, RunErrorEvent,
TextMessageStartEvent, TextMessageContentEvent, TextMessageEndEvent,
ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, StepStartedEvent
(etc.), replacing the old union and adding a short one-line description for each
AGUIEvent type so readers understand the event-based stream architecture.
🧹 Nitpick comments (5)
packages/typescript/ai/tests/generate-types.test-d.ts (1)

433-436: Consider top-level import { z } from 'zod' instead of require.

Using require('zod') as typeof import('zod') in a type-test file is an unusual pattern. Since this is a .test-d.ts file that only checks types (never runs at runtime), a standard import would be cleaner and more idiomatic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai/tests/generate-types.test-d.ts` around lines 433 -
436, Replace the runtime-style require usage for Zod with a top-level TypeScript
import: remove the inline "const { z } = require('zod') as typeof import('zod')"
and instead add a top-level "import { z } from 'zod'" so the test-d.ts file uses
an idiomatic compile-time import; update any references to the local z binding
accordingly (e.g., usages within the describe('chat() with outputSchema')
block).
packages/typescript/ai/src/agent/index.ts (2)

256-267: Multiple as Array<any> casts bypass type safety on messages.

The constructor casts config.options.messages to Array<any> twice — once for extractClientStateFromOriginalMessages (line 259) and once for convertMessagesToModelMessages (line 266). Consider introducing a union type (e.g., Array<ModelMessage | UIMessage>) that both functions accept, or using separate typed parameters to preserve type checking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai/src/agent/index.ts` around lines 256 - 267, The code
repeatedly casts config.options.messages to Array<any>, bypassing type safety;
instead, create a properly typed local variable (e.g., const typedMessages:
Array<ModelMessage | UIMessage>) and use it when calling
extractClientStateFromOriginalMessages and convertMessagesToModelMessages, and
update those function signatures if needed to accept Array<ModelMessage |
UIMessage>; assign the results to this.initialApprovals,
this.initialClientToolResults and this.messages using the typedMessages to
preserve compile-time checks (refer to extractClientStateFromOriginalMessages,
convertMessagesToModelMessages, initialApprovals, initialClientToolResults, and
messages).

1237-1263: Structured output path makes an additional LLM call after the loop completes.

runStructuredAgentLoop first consumes the entire streaming loop (executing all tool calls), then makes a separate textFn() call with outputSchema to extract structured output. This means one extra LLM call per structured agent loop invocation. The design is sound (the final call has full context from tool execution), but this cost implication may be worth documenting for consumers, particularly for expensive models.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai/src/agent/index.ts` around lines 1237 - 1263,
runStructuredAgentLoop currently consumes the streaming AgentLoopEngine and then
makes a separate textFn call with outputSchema, causing an extra LLM call; to
fix, push structured extraction into the engine so the final structured output
is produced as part of the main loop: modify AgentLoopEngine (and its
constructor/run signature) to accept an optional outputSchema and to perform the
final textFn/schema extraction before completing, then remove the standalone
textFn call in runStructuredAgentLoop (use engine.getStructuredResult() or have
engine.getMessages() return the structured result). Update references to
textFn/outputSchema/engine.run/engine.getMessages so the structured extraction
happens inside AgentLoopEngine.
packages/typescript/ai/src/activities/chat/index.ts (1)

280-295: outputSchema as any erases type safety.

The cast on line 294 bypasses the type system entirely. If agentLoop expects a narrower schema type, misuse won't be caught at compile time. Consider using a more precise cast (e.g., as SchemaInput) or adjusting the agentLoop signature to accept the same schema type.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai/src/activities/chat/index.ts` around lines 280 - 295,
The call site casts outputSchema to any which erases type safety; instead update
the agentLoop invocation to preserve the schema type by casting outputSchema to
the expected input type (e.g., use the concrete schema type parameter expected
by agentLoop such as SchemaInput or the generic matching TSchema/TStream) or
update the agentLoop signature to accept the same generic schema type so you can
pass outputSchema without using any; locate the call in agentLoop({...
outputSchema: outputSchema as any }) and either replace the any cast with a
precise cast (e.g., outputSchema as SchemaInput) or change agentLoop's parameter
types so outputSchema: TSchema (or the appropriate generic) is accepted
directly, and ensure the returned type remains TextActivityResult<TSchema,
TStream>.
packages/typescript/ai/src/activities/text/index.ts (1)

352-358: Content accumulation treats chunk.content as a full replacement but falsy check misses empty-string resets.

When chunk.content is an empty string "", the falsy check falls through to accumulatedContent += chunk.delta, which appends rather than resetting. If the adapter ever sends content: "" to signal a reset to empty, it won't take effect. This is likely fine in practice (adapters probably don't send empty-string content), but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai/src/activities/text/index.ts` around lines 352 - 358,
The switch branch for 'TEXT_MESSAGE_CONTENT' treats chunk.content with a falsy
check so an empty string isn't recognized as an explicit reset; change the
condition to check for presence explicitly (e.g., chunk.content !== undefined /
chunk.content != null) so that chunk.content === "" will replace
accumulatedContent (in the code handling chunk and accumulatedContent within the
'TEXT_MESSAGE_CONTENT' case) instead of falling back to appending chunk.delta.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/api/ai.md`:
- Around line 156-161: The agentLoop() docs use Zod-specific types for
outputSchema; update them to match the framework-agnostic terminology used by
text(): change the table entry for `outputSchema` from `z.ZodType` to
`StandardSchema` and update the Returns section from `Promise<z.infer<TSchema>>`
to `Promise<InferSchemaType<TSchema>>` (matching `text()`), ensuring references
to `outputSchema`, `agentLoop()`, and the generic `TSchema` use the same
StandardSchema/InferSchemaType types used elsewhere.
- Around line 130-137: The docs example passes tools incorrectly as tools:
[myTools], which can create a nested array if myTools is already an array;
update the agentLoop call to pass a flat array: if you have multiple tools in an
array named myTools, use tools: myTools; if you have a single tool variable
named myTool, use tools: [myTool]; adjust the example around the agentLoop
invocation and the variable name (myTools vs myTool) accordingly to avoid
[[...tools]].

In `@packages/typescript/ai/src/agent/index.ts`:
- Around line 10-17: Update the relative import statements to consistently use
.js extensions: change imports that reference
'../activities/chat/tools/tool-calls',
'../activities/chat/agent-loop-strategies', '../activities/text/index', and
'../activities/chat/messages' so they end with .js; ensure the lines importing
ToolCallManager and executeToolCalls, maxIterations as maxIterationsStrategy,
text, and convertMessagesToModelMessages are updated to the .js-suffixed paths
to match aiEventClient's '../event-client.js' style.

In `@packages/typescript/ai/tests/generate-types.test-d.ts`:
- Around line 224-230: Several negative type tests in generate-types.test-d.ts
(e.g., the chat({...}) call using model: 'invalid-model') are missing the
required // `@ts-expect-error` annotation; add a single-line // `@ts-expect-error`
comment immediately above every call that is intended to produce a TypeScript
error (search for comments like "invalid model should error", "should not
allow", "should reject", "should NOT accept", "should error on") so Vitest
asserts the line actually errors — apply this to all affected test calls
(including the chat(...) calls and other invalid-usage calls referenced in the
review ranges).

In `@packages/typescript/smoke-tests/adapters/src/tests/emb-embedding.ts`:
- Around line 18-25: The AdapterContext interface in harness.ts is missing
embeddingAdapter and embeddingModel which causes TS errors when emb-embedding.ts
accesses them; update the AdapterContext interface (the same block that defines
summarizeAdapter/summarizeModel and imageAdapter/imageModel) to add
embeddingAdapter?: any and embeddingModel?: string so emb-embedding.ts can
safely read adapterContext.embeddingAdapter and adapterContext.embeddingModel.

In `@testing/panel/tests/helpers.ts`:
- Around line 377-394: The current try/catch around JSON.parse swallows
server-sent errors because throw new Error(parsed.error) is caught locally;
change the flow in the function that handles the SSE chunk (the block using
JSON.parse, parsed, throw new Error, catch) so JSON.parse is wrapped in its own
try/catch and any parse errors are ignored, but the parsed result is then
inspected outside that parse-only catch — if parsed.type === 'error' rethrow or
propagate that error (do not allow it to be swallowed), otherwise continue with
the existing TEXT_MESSAGE_CONTENT handling (chunkCount, summary, provider,
model). Ensure you only catch parse exceptions, not the intentional error
throws.

---

Outside diff comments:
In `@docs/api/ai.md`:
- Around line 396-415: The docs currently list StreamChunk as a union of legacy
types (ContentStreamChunk, ThinkingStreamChunk, ToolCallStreamChunk,
ToolResultStreamChunk, DoneStreamChunk, ErrorStreamChunk); update the
documentation to reflect the current implementation where StreamChunk =
AGUIEvent and describe the AG-UI protocol event types instead — mention concrete
event names like RunStartedEvent, RunFinishedEvent, RunErrorEvent,
TextMessageStartEvent, TextMessageContentEvent, TextMessageEndEvent,
ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, StepStartedEvent
(etc.), replacing the old union and adding a short one-line description for each
AGUIEvent type so readers understand the event-based stream architecture.

---

Nitpick comments:
In `@packages/typescript/ai/src/activities/chat/index.ts`:
- Around line 280-295: The call site casts outputSchema to any which erases type
safety; instead update the agentLoop invocation to preserve the schema type by
casting outputSchema to the expected input type (e.g., use the concrete schema
type parameter expected by agentLoop such as SchemaInput or the generic matching
TSchema/TStream) or update the agentLoop signature to accept the same generic
schema type so you can pass outputSchema without using any; locate the call in
agentLoop({... outputSchema: outputSchema as any }) and either replace the any
cast with a precise cast (e.g., outputSchema as SchemaInput) or change
agentLoop's parameter types so outputSchema: TSchema (or the appropriate
generic) is accepted directly, and ensure the returned type remains
TextActivityResult<TSchema, TStream>.

In `@packages/typescript/ai/src/activities/text/index.ts`:
- Around line 352-358: The switch branch for 'TEXT_MESSAGE_CONTENT' treats
chunk.content with a falsy check so an empty string isn't recognized as an
explicit reset; change the condition to check for presence explicitly (e.g.,
chunk.content !== undefined / chunk.content != null) so that chunk.content ===
"" will replace accumulatedContent (in the code handling chunk and
accumulatedContent within the 'TEXT_MESSAGE_CONTENT' case) instead of falling
back to appending chunk.delta.

In `@packages/typescript/ai/src/agent/index.ts`:
- Around line 256-267: The code repeatedly casts config.options.messages to
Array<any>, bypassing type safety; instead, create a properly typed local
variable (e.g., const typedMessages: Array<ModelMessage | UIMessage>) and use it
when calling extractClientStateFromOriginalMessages and
convertMessagesToModelMessages, and update those function signatures if needed
to accept Array<ModelMessage | UIMessage>; assign the results to
this.initialApprovals, this.initialClientToolResults and this.messages using the
typedMessages to preserve compile-time checks (refer to
extractClientStateFromOriginalMessages, convertMessagesToModelMessages,
initialApprovals, initialClientToolResults, and messages).
- Around line 1237-1263: runStructuredAgentLoop currently consumes the streaming
AgentLoopEngine and then makes a separate textFn call with outputSchema, causing
an extra LLM call; to fix, push structured extraction into the engine so the
final structured output is produced as part of the main loop: modify
AgentLoopEngine (and its constructor/run signature) to accept an optional
outputSchema and to perform the final textFn/schema extraction before
completing, then remove the standalone textFn call in runStructuredAgentLoop
(use engine.getStructuredResult() or have engine.getMessages() return the
structured result). Update references to
textFn/outputSchema/engine.run/engine.getMessages so the structured extraction
happens inside AgentLoopEngine.

In `@packages/typescript/ai/tests/generate-types.test-d.ts`:
- Around line 433-436: Replace the runtime-style require usage for Zod with a
top-level TypeScript import: remove the inline "const { z } = require('zod') as
typeof import('zod')" and instead add a top-level "import { z } from 'zod'" so
the test-d.ts file uses an idiomatic compile-time import; update any references
to the local z binding accordingly (e.g., usages within the describe('chat()
with outputSchema') block).

Comment on lines +130 to +137
for await (const chunk of agentLoop({
adapter: openaiText("gpt-4o"),
messages: [{ role: "user", content: "Complete this task" }],
tools: [myTools],
agentLoopStrategy: maxIterations(10),
})) {
// ...
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

tools: [myTools] likely wraps an array in an array.

If myTools is an array of tools, this creates [[...tools]]. If it's a single tool, the plural name is misleading. Should be either tools: [myTool] or tools: myTools.

📝 Suggested fix
-  tools: [myTools],
+  tools: [myTool],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for await (const chunk of agentLoop({
adapter: openaiText("gpt-4o"),
messages: [{ role: "user", content: "Complete this task" }],
tools: [myTools],
agentLoopStrategy: maxIterations(10),
})) {
// ...
}
for await (const chunk of agentLoop({
adapter: openaiText("gpt-4o"),
messages: [{ role: "user", content: "Complete this task" }],
tools: [myTool],
agentLoopStrategy: maxIterations(10),
})) {
// ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/api/ai.md` around lines 130 - 137, The docs example passes tools
incorrectly as tools: [myTools], which can create a nested array if myTools is
already an array; update the agentLoop call to pass a flat array: if you have
multiple tools in an array named myTools, use tools: myTools; if you have a
single tool variable named myTool, use tools: [myTool]; adjust the example
around the agentLoop invocation and the variable name (myTools vs myTool)
accordingly to avoid [[...tools]].

Comment on lines +156 to +161
| `outputSchema` | `z.ZodType` | For structured output after tool execution |

### Returns

- Default: `AsyncIterable<StreamChunk>` (streaming)
- With `outputSchema`: `Promise<z.infer<TSchema>>`
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Inconsistent schema documentation between text() and agentLoop().

The text() options table (line 85) describes outputSchema as StandardSchema and returns Promise<InferSchemaType<TSchema>>, which is framework-agnostic. The agentLoop() section here uses Zod-specific types (z.ZodType, Promise<z.infer<TSchema>>). Since both APIs support Standard Schema (Zod v4+, ArkType, Valibot per the code), the agentLoop docs should use the same framework-agnostic terminology.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/api/ai.md` around lines 156 - 161, The agentLoop() docs use Zod-specific
types for outputSchema; update them to match the framework-agnostic terminology
used by text(): change the table entry for `outputSchema` from `z.ZodType` to
`StandardSchema` and update the Returns section from `Promise<z.infer<TSchema>>`
to `Promise<InferSchemaType<TSchema>>` (matching `text()`), ensuring references
to `outputSchema`, `agentLoop()`, and the generic `TSchema` use the same
StandardSchema/InferSchemaType types used elsewhere.

Comment on lines +10 to +17
import { aiEventClient } from '../event-client.js'
import {
ToolCallManager,
executeToolCalls,
} from '../activities/chat/tools/tool-calls'
import { maxIterations as maxIterationsStrategy } from '../activities/chat/agent-loop-strategies'
import { text } from '../activities/text/index'
import { convertMessagesToModelMessages } from '../activities/chat/messages'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash

# First, let's see the actual imports in the agent/index.ts file
echo "=== Actual imports in agent/index.ts (lines 10-17) ==="
head -20 packages/typescript/ai/src/agent/index.ts | tail -10

echo ""
echo "=== All imports in agent/index.ts ==="
sed -n '1,30p' packages/typescript/ai/src/agent/index.ts | grep "^import"

echo ""
echo "=== tsconfig configuration ==="
find . -name "tsconfig*.json" -type f | head -3 | xargs -I{} sh -c 'echo "--- {} ---" && cat {}'

Repository: TanStack/ai

Length of output: 2841


🏁 Script executed:

#!/bin/bash

# Find the tsconfig for packages/typescript/ai
echo "=== Looking for tsconfig in packages/typescript/ai ==="
find packages/typescript/ai -name "tsconfig*.json" -type f | xargs -I{} sh -c 'echo "--- {} ---" && cat {}'

echo ""
echo "=== Root tsconfig.json if exists ==="
cat tsconfig.json 2>/dev/null || echo "Not found in root"

echo ""
echo "=== Check packages/typescript/tsconfig ==="
cat packages/typescript/tsconfig.json 2>/dev/null || echo "Not found"

echo ""
echo "=== package.json type field in packages/typescript/ai ==="
grep -A5 '"type"' packages/typescript/ai/package.json 2>/dev/null || echo "Not found"
</parameter>

Repository: TanStack/ai

Length of output: 1997


🏁 Script executed:

#!/bin/bash

# Check import patterns in other files in the same directory
echo "=== Import patterns in packages/typescript/ai/src ==="
find packages/typescript/ai/src -name "*.ts" -o -name "*.tsx" | head -5 | xargs -I{} sh -c 'echo "--- {} ---" && head -20 {} | grep "^import.*from"'

echo ""
echo "=== Check overall import extension consistency in ai package ==="
rg "^import.*from ['\"]\.\." packages/typescript/ai/src --max-count=20 | head -30

Repository: TanStack/ai

Length of output: 3649


Use consistent .js extensions in relative imports.

Line 10 uses '../event-client.js' with the .js extension, while lines 12–17 omit it for '../activities/chat/tools/tool-calls', '../activities/chat/agent-loop-strategies', '../activities/text/index', and '../activities/chat/messages'. For consistency and clarity, extend all relative imports to use .js extensions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai/src/agent/index.ts` around lines 10 - 17, Update the
relative import statements to consistently use .js extensions: change imports
that reference '../activities/chat/tools/tool-calls',
'../activities/chat/agent-loop-strategies', '../activities/text/index', and
'../activities/chat/messages' so they end with .js; ensure the lines importing
ToolCallManager and executeToolCalls, maxIterations as maxIterationsStrategy,
text, and convertMessagesToModelMessages are updated to the .js-suffixed paths
to match aiEventClient's '../event-client.js' style.

Comment on lines +224 to +230
// invalid model should error
chat({
adapter: textAdapter,
model: 'invalid-model',
messages: [{ role: 'user', content: 'Hello' }],
})
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Negative type tests are missing @ts-expect-error annotations.

In .test-d.ts files, calls expected to produce type errors will either (a) fail the type-check entirely if they actually error, preventing all tests from running, or (b) pass silently if the types don't reject them, providing a false sense of safety. Each call that "should error" needs a // @ts-expect-error`` annotation so Vitest verifies the line does produce a type error.

This pattern applies throughout the file — every test case with comments like "invalid model should error", "should not allow", "should reject", "should NOT accept", or "should error on" needs this annotation. A non-exhaustive list of affected lines: 225–229, 243–247, 260–265, 282–291, 308–315, 331–339, 345–351, 356–361, 367–372, 378–383, 401–408, 422–429, 500–513, 515–528, 634–643, 793–801, 803–811, 814–830, 832–848, 850–866, 868–888, 905–913, 926–935, 962–980, 982–1000, 1002–1020, 1052–1070, 1072–1090, 1122–1140, 1202–1211, 1213–1231, 1248–1255, 1268–1276, 1279–1302, 1304–1320, 1322–1345, 1347–1376, 1384–1414, 1416–1446, 1448–1471, 1473–1502, 1509–1540, 1542–1558, 1560–1583, 1585–1623, 1657–1661, 1674–1679, 1693–1697, 1714–1723, 1740–1748, 1761–1769, 1800–1806, 1809–1832.

Example fix for one case:

🔧 Example annotation
     // invalid model should error
+    // `@ts-expect-error` - 'invalid-model' is not a valid model for TestTextAdapter
     chat({
       adapter: textAdapter,
       model: 'invalid-model',
       messages: [{ role: 'user', content: 'Hello' }],
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// invalid model should error
chat({
adapter: textAdapter,
model: 'invalid-model',
messages: [{ role: 'user', content: 'Hello' }],
})
})
// invalid model should error
// `@ts-expect-error` - 'invalid-model' is not a valid model for TestTextAdapter
chat({
adapter: textAdapter,
model: 'invalid-model',
messages: [{ role: 'user', content: 'Hello' }],
})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/ai/tests/generate-types.test-d.ts` around lines 224 -
230, Several negative type tests in generate-types.test-d.ts (e.g., the
chat({...}) call using model: 'invalid-model') are missing the required //
`@ts-expect-error` annotation; add a single-line // `@ts-expect-error` comment
immediately above every call that is intended to produce a TypeScript error
(search for comments like "invalid model should error", "should not allow",
"should reject", "should NOT accept", "should error on") so Vitest asserts the
line actually errors — apply this to all affected test calls (including the
chat(...) calls and other invalid-usage calls referenced in the review ranges).

Comment on lines +18 to +25
if (!adapterContext.embeddingAdapter) {
console.log(
`[${adapterName}] — ${testName}: Ignored (no embedding adapter)`,
)
return { passed: true, ignored: true }
}

const model = adapterContext.embeddingModel || adapterContext.model
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n 'embeddingAdapter|embeddingModel' --type=ts -C3 -g '!node_modules'

Repository: TanStack/ai

Length of output: 2140


🏁 Script executed:

rg -n 'interface AdapterContext' --type=ts -A 30

Repository: TanStack/ai

Length of output: 2687


AdapterContext interface is missing embeddingAdapter and embeddingModel properties.

The AdapterContext interface in harness.ts (lines 48–70) does not include embeddingAdapter or embeddingModel, but the test file at lines 18, 25, and 41 references these properties. This will cause TypeScript compilation errors.

Add the missing properties to the AdapterContext interface following the existing pattern:

  • embeddingAdapter?: any (alongside summarizeAdapter, imageAdapter, etc.)
  • embeddingModel?: string (alongside summarizeModel, imageModel, etc.)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/typescript/smoke-tests/adapters/src/tests/emb-embedding.ts` around
lines 18 - 25, The AdapterContext interface in harness.ts is missing
embeddingAdapter and embeddingModel which causes TS errors when emb-embedding.ts
accesses them; update the AdapterContext interface (the same block that defines
summarizeAdapter/summarizeModel and imageAdapter/imageModel) to add
embeddingAdapter?: any and embeddingModel?: string so emb-embedding.ts can
safely read adapterContext.embeddingAdapter and adapterContext.embeddingModel.

Comment on lines +377 to 394
try {
const parsed = JSON.parse(data)
if (parsed.type === 'error') {
throw new Error(parsed.error)
}
if (parsed.type === 'TEXT_MESSAGE_CONTENT') {
chunkCount++
if (parsed.delta) {
summary += parsed.delta
} else if (parsed.content) {
summary = parsed.content
}
provider = parsed.provider || provider
model = parsed.model || model
}
provider = parsed.provider || provider
model = parsed.model || model
} catch {
// Ignore parse errors
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Server-sent error events are silently swallowed.

The throw new Error(parsed.error) on line 380 is caught by the immediately surrounding catch on line 392 and discarded. A server error response will be treated as a successful (empty) result instead of propagating the error.

🐛 Proposed fix
         try {
           const parsed = JSON.parse(data)
           if (parsed.type === 'error') {
             throw new Error(parsed.error)
           }
           if (parsed.type === 'TEXT_MESSAGE_CONTENT') {
             chunkCount++
             if (parsed.delta) {
               summary += parsed.delta
             } else if (parsed.content) {
               summary = parsed.content
             }
             provider = parsed.provider || provider
             model = parsed.model || model
           }
-        } catch {
-          // Ignore parse errors
+        } catch (e) {
+          // Re-throw application-level errors, ignore JSON parse errors
+          if (e instanceof Error && e.message && !e.message.includes('JSON')) {
+            throw e
+          }
         }

Alternatively, separate the JSON parsing from the error-type check:

         try {
-          const parsed = JSON.parse(data)
-          if (parsed.type === 'error') {
-            throw new Error(parsed.error)
-          }
-          if (parsed.type === 'TEXT_MESSAGE_CONTENT') {
-            chunkCount++
-            if (parsed.delta) {
-              summary += parsed.delta
-            } else if (parsed.content) {
-              summary = parsed.content
-            }
-            provider = parsed.provider || provider
-            model = parsed.model || model
-          }
-        } catch {
-          // Ignore parse errors
+          const parsed = JSON.parse(data)
+          if (parsed.type === 'error') {
+            throw new Error(parsed.error)
+          }
+          if (parsed.type === 'TEXT_MESSAGE_CONTENT') {
+            chunkCount++
+            if (parsed.delta) {
+              summary += parsed.delta
+            } else if (parsed.content) {
+              summary = parsed.content
+            }
+            provider = parsed.provider || provider
+            model = parsed.model || model
+          }
+        } catch (e) {
+          if (e instanceof SyntaxError) continue
+          throw e
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const parsed = JSON.parse(data)
if (parsed.type === 'error') {
throw new Error(parsed.error)
}
if (parsed.type === 'TEXT_MESSAGE_CONTENT') {
chunkCount++
if (parsed.delta) {
summary += parsed.delta
} else if (parsed.content) {
summary = parsed.content
}
provider = parsed.provider || provider
model = parsed.model || model
}
provider = parsed.provider || provider
model = parsed.model || model
} catch {
// Ignore parse errors
}
try {
const parsed = JSON.parse(data)
if (parsed.type === 'error') {
throw new Error(parsed.error)
}
if (parsed.type === 'TEXT_MESSAGE_CONTENT') {
chunkCount++
if (parsed.delta) {
summary += parsed.delta
} else if (parsed.content) {
summary = parsed.content
}
provider = parsed.provider || provider
model = parsed.model || model
}
} catch (e) {
// Re-throw application-level errors, ignore JSON parse errors
if (e instanceof Error && e.message && !e.message.includes('JSON')) {
throw e
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@testing/panel/tests/helpers.ts` around lines 377 - 394, The current try/catch
around JSON.parse swallows server-sent errors because throw new
Error(parsed.error) is caught locally; change the flow in the function that
handles the SSE chunk (the block using JSON.parse, parsed, throw new Error,
catch) so JSON.parse is wrapped in its own try/catch and any parse errors are
ignored, but the parsed result is then inspected outside that parse-only catch —
if parsed.type === 'error' rethrow or propagate that error (do not allow it to
be swallowed), otherwise continue with the existing TEXT_MESSAGE_CONTENT
handling (chunkCount, summary, provider, model). Ensure you only catch parse
exceptions, not the intentional error throws.

@jherr jherr marked this pull request as draft February 19, 2026 18:53
@jherr jherr closed this Feb 19, 2026
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.

2 participants

Comments