Skip to content

feat(plugin): rename transaction/verb to plugin, drop composition + clientTransaction, add media factories#945

Open
AlemTuzlak wants to merge 11 commits into
feat/transaction-client-stubfrom
feat/plugin-api
Open

feat(plugin): rename transaction/verb to plugin, drop composition + clientTransaction, add media factories#945
AlemTuzlak wants to merge 11 commits into
feat/transaction-client-stubfrom
feat/plugin-api

Conversation

@AlemTuzlak

@AlemTuzlak AlemTuzlak commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #942 (feat/transaction-client-stub). This PR keeps the good part of #942 — an app-defined registry of typed capabilities behind one endpoint and one client hook — but reshapes it into a plugin API: it drops the server-side composition layer and the clientTransaction stub, renames transaction/verbplugin, and adds media-generation factories. Server-side composition returns later as a dedicated workflowPlugin.

The differences from #942, in code:


1. Server: defineTransaction/chatVerb/verbdefinePlugin/chatPlugin/generationPlugin (+ media factories)

#942

import { defineTransaction, chatVerb, verb, clientTransaction } from '@tanstack/ai/transaction'

const drafting = chatVerb((req) => chat({ adapter, messages: req.messages, outputSchema: BlogPostSchema }))

const heroImage = verb({
  input: z.object({ prompt: z.string() }),
  execute: async (req, ctx) => generateImage({ adapter, prompt: req.input.prompt }),
})

export const blogTransaction = defineTransaction({ drafting, heroImage, narration })

// a SECOND, type-only client stub mirroring the verb kinds
export const blogTxnDef = clientTransaction<typeof blogTransaction>({
  drafting: 'chat',
  heroImage: 'one-shot',
  narration: 'one-shot',
})

This PR

import { definePlugin, chatPlugin, imagePlugin, speechPlugin } from '@tanstack/ai/plugin'

const drafting = chatPlugin((req) => chat({ adapter, messages: req.messages, outputSchema: BlogPostSchema }))

// media factories over generationPlugin: input + result contract pre-bound
const heroImage = imagePlugin((req) => generateImage({ adapter, prompt: req.input.prompt }))
const narration = speechPlugin((req) => generateSpeech({ adapter, text: req.input.text }))

// one value, no client stub — the client imports this directly
export const blogPlugin = definePlugin({ drafting, heroImage, narration })

New media factories, all thin wrappers over the generic generationPlugin: imagePlugin, videoPlugin, audioPlugin, speechPlugin, transcriptionPlugin, summarizePlugin. The generic escape hatch stays for custom work:

const brief = generationPlugin({
  input: z.object({ topic: z.string() }),
  execute: (req) => summarize({ adapter, text: req.input.topic }),
})

2. Composition removed: ctx.call sub-runs → client-side orchestration

#942 — a "composing verb" ran siblings server-side; each ctx.call streamed back as a tagged sub-run of one request:

const blogPost = verb({
  input: z.object({ topic: z.string() }),
  execute: async ({ input }, ctx) => {            // <- second `ctx` arg
    const draft = await ctx.call(drafting, [{ role: 'user', content: input.topic }])
    const [img, audio] = await Promise.all([
      ctx.call(heroImage, { prompt: heroPromptFor(draft.structured) }),
      ctx.call(narration, { text: draft.text }),
    ])
    return { post: draft.structured, image: img, audio }
  },
})
// client: one call, observe live sub-runs
await txn.blogPost.run({ topic })
const runs = txn.blogPost.subRuns   // [{ verb:'drafting', status, text, partial }, …]

This PRgenerationPlugin.execute takes only req (no ctx); orchestration lives in the component:

// execute signature: (req) => Promise<Result> | AsyncIterable<StreamChunk>   — req.signal still covers abort
const draft = await p.drafting.sendMessage(topic)
await Promise.all([
  p.heroImage.run({ prompt: heroPromptFor(draft) }),
  p.narration.run({ text: forNarration(draft.body) }),
])

Gone with it: TransactionRunContext/ctx.call, the sub-run push channel, TRANSACTION_EVENTS, the client-side sub-run demux, TransactionSubRun, and the subRuns/getSubRuns/onSubRunsChange surfaces. (Server-side composition returns as a future workflowPlugin.)


3. Client: clientTransaction stub + nested verbs map → import the def value + flat options

#942

import { useTransaction } from '@tanstack/ai-react/transaction'
import { blogTxnDef } from '../lib/blog-studio'      // the clientTransaction stub

const txn = useTransaction(blogTxnDef, {
  connection,
  verbs: {                                            // nested per-verb map
    drafting:  { forwardedProps: { tone: 'punchy' } },
    heroImage: { onResult: (img) => save(img) },
  },
})

This PR

import { usePlugin } from '@tanstack/ai-react/plugin'
import { blogPlugin } from '../lib/blog-studio'      // the real definePlugin value

const p = usePlugin(blogPlugin, {
  connection,
  drafting:  { forwardedProps: { tone: 'punchy' } }, // flat, keyed by plugin name
  heroImage: { onResult: (img) => save(img) },
})

The definePlugin value carries plugin names + kinds at runtime, so usePlugin binds off it directly — no second declaration. Importing it into the browser is safe: the adapter callbacks are inert until handler runs server-side, so no credentials leak. Reserved keys (connection/id/threadId) are excluded from the per-plugin option map so a plugin can't collide with them.


Rename at a glance

#942 This PR
defineTransaction definePlugin
chatVerb chatPlugin
verb generationPlugin (+ imagePlugin/videoPlugin/audioPlugin/speechPlugin/transcriptionPlugin/summarizePlugin)
useTransaction / createTransaction usePlugin / createPlugin
clientTransaction removed — bind off the definePlugin value
TransactionClient PluginClient
execute(req, ctx) + ctx.call execute(req) + client-side orchestration
TRANSACTION_EVENTS, sub-runs, subRuns removed (returning via workflowPlugin)
def.verbs / verbKinds / ~verbs def.plugins / pluginKinds / ~plugins
subpath @tanstack/ai*/transaction @tanstack/ai*/plugin

Media factory input schemas are hand-rolled Standard Schemas (zod-free) so the core stays schema-library-agnostic — no runtime zod dependency is pulled into the bundle.

Test plan

  • pnpm test:pr (CI canonical gate)
  • pnpm --filter @tanstack/ai-e2e test:e2e

Local (this branch): each changed package passed test:types + test:lib; consumers ts-react-chat + @tanstack/ai-e2e passed test:types against the final API; e2e plugin suite 3/3 (chat / one-shot / media); test:docs, test:kiira (794/794), test:knip, test:sherif green. The one-shot full test:pr + full e2e were not completed locally (nx-daemon/memory stall on the dev machine — infra, not code), hence relying on CI.

Follow-up (non-blocking)

runGenerationPluginStream's non-streaming branch could delegate to the existing streamGenerationResult helper (byte-identical behavior today) — small DRY cleanup left for later.

🤖 Generated with Claude Code

The media factories used z.object(...) at runtime, the first entry-reachable
runtime zod import in the package. Since zod is only a devDependency of this
deliberately schema-library-agnostic package, that bundled zod into the ESM
output (dist/esm/node_modules/zod) and shifted Rollup's preserveModules root,
nesting all emitted JS under dist/esm/packages/ai/src. Replace the six z.object
schemas with a hand-rolled Standard Schema helper (object + required-key check),
keeping the same public input types and typed req.input.
…ePlugin, flatten options, drop subRuns (solid/vue/svelte)
@coderabbitai

coderabbitai Bot commented Jul 15, 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 05e84d12-d93f-45cd-ab98-364ae4d39d5f

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/plugin-api

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.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

19 package(s) bumped directly, 26 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-angular 0.2.3 → 1.0.0 Changeset
@tanstack/ai-anthropic 0.16.1 → 1.0.0 Changeset
@tanstack/ai-bedrock 0.1.2 → 1.0.0 Changeset
@tanstack/ai-fal 0.9.10 → 1.0.0 Changeset
@tanstack/ai-gemini 0.19.1 → 1.0.0 Changeset
@tanstack/ai-grok 0.14.7 → 1.0.0 Changeset
@tanstack/ai-groq 0.5.1 → 1.0.0 Changeset
@tanstack/ai-mistral 0.2.1 → 1.0.0 Changeset
@tanstack/ai-ollama 0.8.14 → 1.0.0 Changeset
@tanstack/ai-openai 0.16.0 → 1.0.0 Changeset
@tanstack/ai-openrouter 0.15.8 → 1.0.0 Changeset
@tanstack/ai-preact 0.10.3 → 1.0.0 Changeset
@tanstack/ai-react 0.16.4 → 1.0.0 Changeset
@tanstack/ai-sandbox 0.2.2 → 1.0.0 Changeset
@tanstack/ai-solid 0.14.3 → 1.0.0 Changeset
@tanstack/ai-svelte 0.14.3 → 1.0.0 Changeset
@tanstack/ai-vue 0.14.3 → 1.0.0 Changeset
@tanstack/ai-acp 0.2.1 → 1.0.0 Dependent
@tanstack/ai-claude-code 0.2.1 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.3.6 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.3.9 → 1.0.0 Dependent
@tanstack/ai-codex 0.2.1 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.32 → 1.0.0 Dependent
@tanstack/ai-grok-build 0.2.1 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.45 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.45 → 1.0.0 Dependent
@tanstack/ai-opencode 0.2.1 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.13 → 1.0.0 Dependent
@tanstack/ai-sandbox-cloudflare 0.2.2 → 1.0.0 Dependent
@tanstack/ai-sandbox-daytona 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-docker 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-local-process 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-sprites 0.2.1 → 1.0.0 Dependent
@tanstack/ai-sandbox-vercel 0.2.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.12 → 1.0.0 Dependent
@tanstack/openai-base 0.9.7 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.40.0 → 0.41.0 Changeset
@tanstack/ai-client 0.20.0 → 0.21.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-devtools-core 0.4.22 → 0.4.23 Dependent
@tanstack/ai-isolate-cloudflare 0.2.36 → 0.2.37 Dependent
@tanstack/ai-mcp 0.2.3 → 0.2.4 Dependent
@tanstack/ai-vue-ui 0.2.31 → 0.2.32 Dependent
@tanstack/preact-ai-devtools 0.1.65 → 0.1.66 Dependent
@tanstack/react-ai-devtools 0.2.65 → 0.2.66 Dependent
@tanstack/solid-ai-devtools 0.2.65 → 0.2.66 Dependent

@nx-cloud

nx-cloud Bot commented Jul 15, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 250d506


☁️ Nx Cloud last updated this comment at 2026-07-15 12:34:20 UTC

@nx-cloud

nx-cloud Bot commented Jul 15, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 250d506

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ❌ Failed 12m 39s View ↗
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 2m 15s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-15 12:48:12 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

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

@tanstack/ai-acp

npm i https://pkg.pr.new/@tanstack/ai-acp@945

@tanstack/ai-angular

npm i https://pkg.pr.new/@tanstack/ai-angular@945

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

npm i https://pkg.pr.new/@tanstack/ai-bedrock@945

@tanstack/ai-claude-code

npm i https://pkg.pr.new/@tanstack/ai-claude-code@945

@tanstack/ai-client

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

@tanstack/ai-code-mode

npm i https://pkg.pr.new/@tanstack/ai-code-mode@945

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/@tanstack/ai-code-mode-skills@945

@tanstack/ai-codex

npm i https://pkg.pr.new/@tanstack/ai-codex@945

@tanstack/ai-devtools-core

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

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/@tanstack/ai-elevenlabs@945

@tanstack/ai-event-client

npm i https://pkg.pr.new/@tanstack/ai-event-client@945

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

npm i https://pkg.pr.new/@tanstack/ai-grok-build@945

@tanstack/ai-groq

npm i https://pkg.pr.new/@tanstack/ai-groq@945

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-isolate-cloudflare@945

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/@tanstack/ai-isolate-node@945

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/@tanstack/ai-isolate-quickjs@945

@tanstack/ai-mcp

npm i https://pkg.pr.new/@tanstack/ai-mcp@945

@tanstack/ai-mistral

npm i https://pkg.pr.new/@tanstack/ai-mistral@945

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

npm i https://pkg.pr.new/@tanstack/ai-opencode@945

@tanstack/ai-openrouter

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

npm i https://pkg.pr.new/@tanstack/ai-sandbox@945

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/@tanstack/ai-sandbox-cloudflare@945

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/@tanstack/ai-sandbox-daytona@945

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/@tanstack/ai-sandbox-docker@945

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/@tanstack/ai-sandbox-local-process@945

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/@tanstack/ai-sandbox-sprites@945

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/@tanstack/ai-sandbox-vercel@945

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

npm i https://pkg.pr.new/@tanstack/ai-utils@945

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

npm i https://pkg.pr.new/@tanstack/openai-base@945

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: 250d506

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