') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [ai] Reduce doStreamStep step boundary payload by VaguelySerious · Pull Request #1932 · vercel/workflow · GitHub
Skip to content

[ai] Reduce doStreamStep step boundary payload - #1932

Merged
VaguelySerious merged 8 commits into
mainfrom
peter/issue-1929-reduce-stream-step-payload
Jun 26, 2026
Merged

[ai] Reduce doStreamStep step boundary payload#1932
VaguelySerious merged 8 commits into
mainfrom
peter/issue-1929-reduce-stream-step-payload

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented May 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Move chunksToStep out of the doStreamStep step boundary. The step now returns minimal raw aggregates (text, reasoning, files, sources, warnings, responseMetadata, rawFinishReason, usage, providerMetadata); the full StepResult is reconstructed in streamTextIterator (workflow context) via buildStepResult.
  • Drop the separate finish field from the return — every field it carried was already on the reconstructed StepResult.
  • The user-facing StepResult exposed via onStepFinish and result.steps[] is unchanged. The reduction is purely in the durable execution event log.

What's no longer crossing the boundary:

  • StepResult's redundant tool-call lists (dynamicToolCalls, staticToolCalls, staticToolResults, dynamicToolResults)
  • Always-empty toolResults and response.messages
  • Duplicate content and reasoningText aggregates
  • Dual base64 + Uint8Array file encoding (expanded only after the boundary)
  • request.body (a JSON dump of the input prompt the caller already has)
  • finish (its finishReason, usage, providerMetadata already on step)

Internally, the V3 chunks[] array inside the step is also gone — fields are aggregated directly during the streaming transform.

Behavior change worth noting

response-metadata is now merged per-field across chunks instead of first-wins. The old chunksToStep did chunks.find(c => c.type === 'response-metadata') (first chunk wins for all fields); reconstruction now merges each field with last-non-null-wins, matching the AI SDK. For providers that split metadata across multiple chunks this can change the reconstructed response.id / response.modelId / response.timestamp and the derived model.provider/model.modelId. This is a fix, not a regression, and is covered by a new test in do-stream-step.test.ts.

Closes#1929.

Test plan

  • pnpm --filter @workflow/ai test — 187 passing, including updated mocks for do-stream-step.test.ts and stream-text-iterator.test.ts
  • pnpm --filter @workflow/ai build — clean tsc
  • CI green
  • Spot-check with workbench example (manual; not done locally — UI behavior should be unchanged since the user-facing StepResult shape is identical)

🤖 Generated with Claude Code


Ported to @ai-sdk/workflow:vercel/ai#16434

Move chunksToStep out of the step boundary, returning minimal raw
aggregates (text, reasoning, files, sources, warnings, response
metadata, finishReason, usage, providerMetadata) and reconstructing
the full StepResult in streamTextIterator (workflow context). Also
drops the separate `finish` field from the return — its data was
fully duplicated by the StepResult fields the caller already needed.
The user-facing StepResult exposed via `onStepFinish` and `steps[]`
is unchanged. The event-log payload no longer carries:
- StepResult's redundant tool-call lists (`dynamicToolCalls`,
`staticToolCalls`, `staticToolResults`, `dynamicToolResults`)
- The always-empty `toolResults` and `response.messages` arrays
- The duplicate `content` and `reasoningText` aggregates
- The dual base64 + Uint8Array file encoding (now expanded only
after crossing the boundary)
- `request.body` (a JSON dump of the input prompt the caller already
has) and `finish` (its fields all appear on `step`)
Closes#1929.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vercel

vercelBot commented May 5, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented May 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e4066d4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
NameType
@workflow/aiPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions

github-actionsBot commented May 5, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production144202301672
✅ 💻 Local Development160502191824
✅ 📦 Local Production160502191824
✅ 🐘 Local Postgres159302311824
✅ 🪟 Windows15200152
✅ 📋 Other88501791064
Total7282010788360

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro125027
✅ example125027
✅ express125027
✅ fastify125027
✅ hono125027
✅ nextjs-turbopack14903
✅ nextjs-webpack14903
✅ nitro125027
✅ nuxt125027
✅ sveltekit14408
✅ vite125027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable127025
✅ express-stable127025
✅ fastify-stable127025
✅ hono-stable127025
✅ nextjs-turbopack-canary133019
✅ nextjs-turbopack-stable15200
✅ nextjs-webpack-canary133019
✅ nextjs-webpack-stable15200
✅ nitro-stable127025
✅ nuxt-stable127025
✅ sveltekit-stable14606
✅ vite-stable127025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable126026
✅ express-stable126026
✅ fastify-stable126026
✅ hono-stable126026
✅ nextjs-turbopack-canary132020
✅ nextjs-turbopack-stable15101
✅ nextjs-webpack-canary132020
✅ nextjs-webpack-stable15101
✅ nitro-stable126026
✅ nuxt-stable126026
✅ sveltekit-stable14507
✅ vite-stable126026
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15200
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable127025
✅ e2e-local-dev-tanstack-start-127025
✅ e2e-local-postgres-nest-stable126026
✅ e2e-local-postgres-tanstack-start-126026
✅ e2e-local-prod-nest-stable127025
✅ e2e-local-prod-tanstack-start-127025
✅ e2e-vercel-prod-tanstack-start125027

📋 View full workflow run

@github-actions

github-actionsBot commented May 5, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.049s (+4.3%)1.007s (~)0.958s101.00x
💻 LocalExpress0.051s (+8.5% 🔺)1.006s (~)0.955s101.04x
💻 LocalNext.js (Turbopack)0.056s (+4.5%)1.006s (~)0.950s101.13x
🐘 PostgresNext.js (Turbopack)0.057s (~)1.011s (~)0.954s101.17x
🐘 PostgresExpress0.067s (-1.0%)1.013s (~)0.945s101.37x
🐘 PostgresNitro0.083s (+25.0% 🔺)1.026s (+1.4%)0.943s101.69x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.288s (+29.4% 🔺)2.110s (+25.1% 🔺)1.822s101.00x
▲ VercelNitro0.348s (+33.6% 🔺)2.308s (~)1.959s101.21x
▲ VercelNext.js (Turbopack)0.464s (-42.7% 🟢)2.188s (-9.0% 🟢)1.724s101.61x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.088s (+0.7%)2.007s (~)0.920s101.00x
💻 LocalNext.js (Turbopack)1.090s (~)2.007s (~)0.916s101.00x
💻 LocalExpress1.091s (+0.8%)2.007s (~)0.915s101.00x
🐘 PostgresNext.js (Turbopack)1.093s (~)2.010s (~)0.917s101.00x
🐘 PostgresExpress1.099s (~)2.009s (~)0.910s101.01x
🐘 PostgresNitro1.099s (~)2.009s (~)0.910s101.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.456s (~)3.264s (-8.6% 🟢)1.808s101.00x
▲ VercelNitro1.511s (+6.0% 🔺)3.228s (+5.8% 🔺)1.716s101.04x
▲ VercelNext.js (Turbopack)2.440s (+9.4% 🔺)4.150s (+9.0% 🔺)1.710s101.68x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express10.464s (~)11.023s (~)0.559s31.00x
🐘 PostgresExpress10.468s (~)11.015s (~)0.548s31.00x
💻 LocalNext.js (Turbopack)10.482s (~)11.023s (~)0.541s31.00x
💻 LocalNitro10.498s (~)11.023s (~)0.525s31.00x
🐘 PostgresNitro10.526s (~)11.019s (~)0.493s31.01x
🐘 PostgresNext.js (Turbopack)10.543s (+0.5%)11.014s (~)0.471s31.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express11.973s (~)13.483s (-3.2%)1.510s31.00x
▲ VercelNitro12.030s (+2.9%)13.846s (~)1.816s31.00x
▲ VercelNext.js (Turbopack)13.399s (+7.5% 🔺)15.254s (+5.9% 🔺)1.854s21.12x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.668s (~)14.029s (~)0.361s51.00x
💻 LocalExpress13.669s (~)14.028s (~)0.359s51.00x
💻 LocalNext.js (Turbopack)13.696s (~)14.027s (~)0.331s51.00x
🐘 PostgresNitro13.702s (~)14.019s (~)0.317s51.00x
🐘 PostgresExpress13.740s (~)14.023s (~)0.283s51.01x
🐘 PostgresNext.js (Turbopack)13.764s (~)14.018s (~)0.253s51.01x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.270s (+4.9%)19.233s (+2.9%)1.963s41.00x
▲ VercelExpress17.643s (-2.4%)19.791s (-3.2%)2.147s41.02x
▲ VercelNext.js (Turbopack)18.534s (+1.0%)19.987s (-1.4%)1.453s41.07x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro12.188s (-0.6%)13.018s (~)0.830s71.00x
🐘 PostgresExpress12.219s (~)13.019s (~)0.801s71.00x
💻 LocalNitro12.277s (~)13.027s (~)0.750s71.01x
🐘 PostgresNext.js (Turbopack)12.290s (~)13.016s (~)0.725s71.01x
💻 LocalExpress12.355s (~)13.025s (~)0.670s71.01x
💻 LocalNext.js (Turbopack)12.458s (+1.3%)13.025s (~)0.567s71.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro24.066s (+31.8% 🔺)26.139s (+28.0% 🔺)2.073s41.00x
▲ VercelExpress24.502s (+31.5% 🔺)26.618s (+26.4% 🔺)2.116s41.02x
▲ VercelNext.js (Turbopack)24.885s (+17.3% 🔺)26.679s (+15.8% 🔺)1.794s41.03x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.184s (~)2.008s (~)0.824s151.00x
🐘 PostgresNitro1.191s (+1.0%)2.009s (~)0.817s151.01x
🐘 PostgresNext.js (Turbopack)1.192s (+1.1%)2.008s (~)0.816s151.01x
💻 LocalNext.js (Turbopack)1.425s (-6.6% 🟢)2.006s (-3.2%)0.581s151.20x
💻 LocalExpress1.426s (-11.7% 🟢)2.007s (-6.6% 🟢)0.581s151.20x
💻 LocalNitro1.477s (+4.7%)2.007s (~)0.529s151.25x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.540s (-30.9% 🟢)4.145s (-24.7% 🟢)1.605s81.00x
▲ VercelNitro2.553s (+22.2% 🔺)4.109s (+4.9%)1.556s81.01x
▲ VercelNext.js (Turbopack)4.730s (+33.0% 🔺)6.258s (+26.1% 🔺)1.528s51.86x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.313s (-1.0%)3.110s (+3.4%)1.797s101.00x
🐘 PostgresNitro1.366s (+3.4%)2.470s (-7.6% 🟢)1.105s131.04x
🐘 PostgresExpress1.393s (+4.9%)2.593s (~)1.200s121.06x
💻 LocalExpress2.503s (+5.3% 🔺)3.009s (~)0.505s101.91x
💻 LocalNitro2.576s (+3.7%)3.109s (~)0.533s101.96x
💻 LocalNext.js (Turbopack)2.746s (~)3.209s (+6.7% 🔺)0.463s102.09x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.223s (+16.8% 🔺)5.001s (+13.7% 🔺)1.778s61.00x
▲ VercelExpress3.595s (-12.3% 🟢)5.443s (-10.8% 🟢)1.848s61.12x
▲ VercelNext.js (Turbopack)5.447s (-14.3% 🟢)7.479s (-7.8% 🟢)2.032s51.69x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.624s (+2.6%)4.011s (-3.0%)2.387s81.00x
🐘 PostgresExpress1.653s (+2.0%)4.298s (+3.9%)2.646s71.02x
🐘 PostgresNext.js (Turbopack)3.761s (+39.2% 🔺)6.420s (+12.9% 🔺)2.659s52.32x
💻 LocalExpress6.335s (+2.0%)6.817s (~)0.481s53.90x
💻 LocalNitro6.911s (+1.5%)7.767s (+4.7%)0.856s44.26x
💻 LocalNext.js (Turbopack)7.123s (-7.3% 🟢)8.519s (~)1.396s44.39x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.356s (+35.3% 🔺)6.339s (+23.3% 🔺)1.983s51.00x
▲ VercelExpress4.487s (+37.1% 🔺)6.465s (+25.1% 🔺)1.978s51.03x
▲ VercelNext.js (Turbopack)5.669s (+14.1% 🔺)7.593s (+9.2% 🔺)1.924s41.30x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.176s (~)2.008s (~)0.832s151.00x
🐘 PostgresExpress1.190s (~)2.008s (~)0.818s151.01x
🐘 PostgresNitro1.194s (~)2.007s (~)0.812s151.02x
💻 LocalExpress1.430s (+1.2%)2.006s (~)0.576s151.22x
💻 LocalNext.js (Turbopack)1.435s (-3.0%)2.007s (~)0.573s151.22x
💻 LocalNitro1.453s (-1.9%)2.007s (~)0.554s151.24x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.720s (+31.6% 🔺)4.470s (+16.3% 🔺)1.750s71.00x
▲ VercelNitro3.091s (+42.5% 🔺)4.704s (+12.9% 🔺)1.613s71.14x
▲ VercelNext.js (Turbopack)4.370s (+28.1% 🔺)6.080s (+13.8% 🔺)1.710s51.61x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.314s (-1.2%)2.509s (+8.3% 🔺)1.195s121.00x
🐘 PostgresNext.js (Turbopack)1.324s (+1.1%)3.110s (+6.6% 🔺)1.786s101.01x
🐘 PostgresNitro1.345s (+3.9%)2.317s (+4.3%)0.972s131.02x
💻 LocalExpress2.531s (-4.2%)3.109s (~)0.578s101.93x
💻 LocalNitro2.638s (+11.7% 🔺)3.210s (+6.6% 🔺)0.572s102.01x
💻 LocalNext.js (Turbopack)2.649s (+4.1%)3.209s (+6.6% 🔺)0.560s102.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.453s (+47.8% 🔺)5.041s (+25.4% 🔺)1.589s61.00x
▲ VercelExpress4.420s (+92.8% 🔺)6.466s (+63.6% 🔺)2.046s51.28x
▲ VercelNext.js (Turbopack)5.529s (+46.8% 🔺)7.534s (+45.0% 🔺)2.006s41.60x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.630s (+0.6%)4.297s (+7.1% 🔺)2.667s71.00x
🐘 PostgresExpress1.644s (-4.2%)4.136s (-3.0%)2.492s81.01x
🐘 PostgresNext.js (Turbopack)3.201s (+11.3% 🔺)5.852s (-2.7%)2.651s61.96x
💻 LocalNitro6.335s (-10.1% 🟢)7.016s (-6.7% 🟢)0.681s53.89x
💻 LocalExpress6.980s (-1.0%)7.517s (-3.2%)0.537s44.28x
💻 LocalNext.js (Turbopack)7.739s (+12.2% 🔺)8.521s (+11.9% 🔺)0.781s44.75x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro4.442s (+19.1% 🔺)6.275s (+8.9% 🔺)1.833s51.00x
▲ VercelExpress4.539s (+34.4% 🔺)6.301s (+20.1% 🔺)1.762s51.02x
▲ VercelNext.js (Turbopack)5.528s (+25.4% 🔺)7.343s (+22.3% 🔺)1.815s51.24x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.551s (-7.4% 🟢)1.006s (-1.7%)0.455s601.00x
🐘 PostgresNext.js (Turbopack)0.553s (+1.1%)1.007s (~)0.454s601.00x
💻 LocalNitro0.580s (-1.9%)1.005s (-1.7%)0.425s601.05x
💻 LocalExpress0.585s (-2.7%)1.005s (~)0.420s601.06x
🐘 PostgresNitro0.599s (+13.0% 🔺)1.041s (+3.5%)0.442s581.09x
💻 LocalNext.js (Turbopack)0.630s (+2.7%)1.005s (~)0.376s601.14x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.350s (+54.5% 🔺)6.012s (+32.5% 🔺)1.662s101.00x
▲ VercelNitro5.393s (+95.4% 🔺)7.258s (+61.7% 🔺)1.865s91.24x
▲ VercelNext.js (Turbopack)5.557s (+31.3% 🔺)7.100s (+18.9% 🔺)1.543s91.28x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.327s (+3.3%)2.007s (~)0.681s451.00x
🐘 PostgresExpress1.331s (+0.7%)2.030s (+1.1%)0.699s451.00x
🐘 PostgresNitro1.397s (+7.4% 🔺)2.030s (+1.1%)0.633s451.05x
💻 LocalNitro1.482s (+2.2%)2.006s (~)0.523s451.12x
💻 LocalExpress1.507s (+1.5%)2.007s (~)0.500s451.14x
💻 LocalNext.js (Turbopack)1.554s (+0.9%)2.029s (~)0.475s451.17x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.158s (+41.4% 🔺)10.764s (+31.3% 🔺)1.607s91.00x
▲ VercelExpress9.566s (+28.0% 🔺)11.363s (+23.7% 🔺)1.797s81.04x
▲ VercelNext.js (Turbopack)11.885s (+22.7% 🔺)13.539s (+18.5% 🔺)1.654s71.30x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.491s (-7.8% 🟢)3.008s (-3.3%)0.517s401.00x
🐘 PostgresNext.js (Turbopack)2.682s (+1.1%)3.009s (-1.6%)0.326s401.08x
🐘 PostgresNitro2.778s (+7.9% 🔺)3.138s (+0.9%)0.360s391.12x
💻 LocalNitro3.180s (+1.6%)3.945s (~)0.765s311.28x
💻 LocalExpress3.239s (-1.4%)4.010s (-0.8%)0.770s301.30x
💻 LocalNext.js (Turbopack)3.314s (~)4.043s (+0.9%)0.729s301.33x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express18.680s (+9.6% 🔺)20.821s (+9.9% 🔺)2.142s61.00x
▲ VercelNitro19.736s (+34.3% 🔺)21.545s (+29.0% 🔺)1.810s61.06x
▲ VercelNext.js (Turbopack)23.082s (+9.8% 🔺)24.680s (+8.7% 🔺)1.598s51.24x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.173s (-7.1% 🟢)1.006s (~)0.833s601.00x
🐘 PostgresExpress0.219s (+1.9%)1.006s (~)0.787s601.26x
🐘 PostgresNitro0.222s (+4.5%)1.006s (~)0.785s601.28x
💻 LocalExpress0.453s (-0.6%)1.005s (~)0.551s602.62x
💻 LocalNitro0.462s (+6.1% 🔺)1.005s (~)0.543s602.67x
💻 LocalNext.js (Turbopack)0.631s (+1.7%)1.022s (+1.7%)0.391s593.64x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.598s (-2.6%)3.068s (-10.4% 🟢)1.470s201.00x
▲ VercelExpress1.604s (-6.7% 🟢)3.195s (-10.0% 🟢)1.591s191.00x
▲ VercelNext.js (Turbopack)2.736s (+25.1% 🔺)4.485s (+15.8% 🔺)1.749s141.71x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.306s (+10.7% 🔺)1.041s (+3.4%)0.735s871.00x
🐘 PostgresNitro0.328s (+1.5%)1.007s (-1.1%)0.678s901.07x
🐘 PostgresExpress0.335s (+0.8%)1.006s (~)0.671s901.10x
💻 LocalExpress2.167s (~)2.736s (-2.2%)0.569s337.09x
💻 LocalNitro2.215s (+5.6% 🔺)2.767s (+1.1%)0.552s337.24x
💻 LocalNext.js (Turbopack)2.723s (-5.2% 🟢)3.343s (-2.2%)0.620s278.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.955s (+22.2% 🔺)3.639s (+11.8% 🔺)1.684s251.00x
▲ VercelExpress2.082s (+14.4% 🔺)3.811s (+8.6% 🔺)1.728s241.07x
▲ VercelNext.js (Turbopack)3.411s (+20.9% 🔺)4.942s (+11.5% 🔺)1.531s191.74x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.504s (~)3.059s (+1.7%)2.555s401.00x
🐘 PostgresNitro0.537s (+2.4%)1.088s (+3.6%)0.551s1111.06x
🐘 PostgresExpress0.555s (+4.2%)1.160s (+8.6% 🔺)0.605s1041.10x
💻 LocalNitro9.850s (+1.1%)10.862s (~)1.011s1219.52x
💻 LocalExpress9.949s (-0.6%)10.863s (-2.3%)0.914s1219.72x
💻 LocalNext.js (Turbopack)10.656s (+4.8%)11.666s (+1.6%)1.010s1121.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.859s (+24.9% 🔺)4.546s (+7.3% 🔺)1.687s271.00x
▲ VercelNitro3.051s (+30.5% 🔺)4.804s (+15.0% 🔺)1.753s251.07x
▲ VercelNext.js (Turbopack)5.440s (+39.6% 🔺)7.297s (+29.8% 🔺)1.857s171.90x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.143s (~)1.967s (~)0.013s (+8.5% 🔺)2.020s (~)0.877s101.00x
💻 LocalExpress1.161s (-2.5%)2.006s (~)0.012s (-3.2%)2.021s (~)0.860s101.02x
💻 LocalNitro1.161s (~)2.005s (~)0.012s (-0.8%)2.020s (~)0.859s101.02x
🐘 PostgresNext.js (Turbopack)1.165s (+0.8%)2.001s (~)0.001s (+9.1% 🔺)2.012s (~)0.848s101.02x
🐘 PostgresExpress1.165s (-0.7%)1.999s (~)0.001s (-25.0% 🟢)2.010s (~)0.845s101.02x
🐘 PostgresNitro1.166s (+1.1%)1.995s (~)0.002s (+7.1% 🔺)2.010s (~)0.844s101.02x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.191s (~)3.364s (-4.4%)3.266s (+69.1% 🔺)7.190s (+20.3% 🔺)4.999s101.00x
▲ VercelExpress2.385s (+19.5% 🔺)3.349s (-12.5% 🟢)3.546s (+41.6% 🔺)7.521s (+10.0% 🔺)5.136s101.09x
▲ VercelNext.js (Turbopack)3.890s (+10.2% 🔺)3.836s (-16.6% 🟢)2.997s (+22.2% 🔺)8.363s (~)4.473s101.78x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.567s (-2.0%)2.003s (~)0.005s (+3.4%)2.025s (~)0.458s301.00x
💻 LocalExpress1.567s (~)2.010s (~)0.012s (+3.5%)2.025s (~)0.458s301.00x
💻 LocalNitro1.577s (~)2.010s (~)0.012s (+1.1%)2.026s (~)0.449s301.01x
🐘 PostgresNext.js (Turbopack)1.591s (~)2.008s (~)0.005s (-3.9%)2.025s (~)0.434s301.02x
💻 LocalNext.js (Turbopack)1.600s (+0.9%)1.971s (~)0.012s (+3.2%)2.025s (~)0.425s301.02x
🐘 PostgresNitro1.628s (+5.1% 🔺)2.003s (~)0.005s (~)2.027s (~)0.399s301.04x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express7.651s (+31.1% 🔺)9.220s (+25.7% 🔺)0.313s (-2.1%)10.276s (+26.1% 🔺)2.626s61.00x
▲ VercelNitro7.785s (+37.6% 🔺)9.031s (+21.7% 🔺)0.513s (+20.9% 🔺)10.176s (+21.3% 🔺)2.391s61.02x
▲ VercelNext.js (Turbopack)11.376s (+24.7% 🔺)11.696s (+15.8% 🔺)0.401s (+73.6% 🔺)13.287s (+20.9% 🔺)1.911s51.49x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.788s (~)1.103s (+1.8%)0.000s (+1.9%)1.118s (+1.9%)0.331s541.00x
🐘 PostgresNitro0.843s (+7.9% 🔺)1.147s (+4.2%)0.000s (+217.3% 🔺)1.162s (+4.1%)0.319s521.07x
🐘 PostgresNext.js (Turbopack)0.991s (+2.4%)1.461s (+7.8% 🔺)0.000s (-45.1% 🟢)1.471s (+7.9% 🔺)0.480s411.26x
💻 LocalExpress1.269s (-3.2%)2.013s (~)0.000s (+80.0% 🔺)2.016s (~)0.747s301.61x
💻 LocalNitro1.333s (+10.2% 🔺)2.015s (~)0.000s (+71.4% 🔺)2.018s (~)0.685s301.69x
💻 LocalNext.js (Turbopack)1.412s (+2.8%)1.979s (~)0.000s (-52.9% 🟢)2.017s (~)0.605s301.79x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.155s (+29.5% 🔺)5.569s (+20.5% 🔺)0.000s (NaN%)6.112s (+18.4% 🔺)1.957s101.00x
▲ VercelNitro4.263s (+24.0% 🔺)5.351s (+6.9% 🔺)0.008s (+Infinity% 🔺)5.929s (+8.0% 🔺)1.665s111.03x
▲ VercelNext.js (Turbopack)5.277s (+17.8% 🔺)5.654s (+4.3%)0.000s (NaN%)6.720s (+6.8% 🔺)1.443s91.27x

🔍 Observability: Express | Nitro | Next.js (Turbopack)

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.715s (+3.0%)2.244s (~)0.000s (-51.8% 🟢)2.282s (~)0.567s281.00x
🐘 PostgresExpress1.775s (-2.5%)2.336s (-2.4%)0.000s (NaN%)2.356s (-2.2%)0.581s261.03x
🐘 PostgresNext.js (Turbopack)2.615s (-5.1% 🟢)3.160s (-5.2% 🟢)0.000s (-5.3% 🟢)3.168s (-5.2% 🟢)0.553s191.52x
💻 LocalNext.js (Turbopack)3.354s (-8.8% 🟢)3.803s (-7.8% 🟢)0.001s (+167.9% 🔺)3.851s (-7.6% 🟢)0.497s161.96x
💻 LocalExpress3.600s (+7.1% 🔺)4.161s (+5.0%)0.001s (-20.0% 🟢)4.165s (+4.9%)0.565s152.10x
💻 LocalNitro3.620s (-1.2%)4.160s (-1.5%)0.001s (+88.9% 🔺)4.164s (-1.6%)0.543s152.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro5.587s (-2.5%)6.808s (-3.7%)0.000s (-100.0% 🟢)7.318s (-3.8%)1.731s91.00x
▲ VercelExpress5.824s (+9.9% 🔺)6.980s (+7.1% 🔺)0.000s (NaN%)7.510s (+7.1% 🔺)1.686s81.04x
▲ VercelNext.js (Turbopack)9.441s (+23.5% 🔺)9.651s (+16.1% 🔺)0.000s (NaN%)10.729s (+12.7% 🔺)1.288s61.69x

🔍 Observability: Nitro | Express | Next.js (Turbopack)

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro9/21
🐘 PostgresNext.js (Turbopack)9/21
▲ VercelNitro11/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres16/21
Next.js (Turbopack)🐘 Postgres16/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Redis + BullMQ: Community world (local development)
  • 🌐 Cloudflare: Community world (local development)
  • 🌐 MySQL: Community world (local development)
  • 🌐 Azure: Community world (local development)
  • 🌐 NATS JetStream: Community world (local development)
  • 🌐 Upstash: Community world (local development)
  • 🌐 Platformatic: Community world (local development)

📋 View full workflow run

warnings = chunk.warnings;
break;
case 'response-metadata':
responseMetadata = {

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.

AI Review: This now replaces the entire response metadata object on every response-metadata chunk. Those fields are optional, and AI SDK stream handling treats metadata as cumulative, so a later partial chunk can drop an earlier id/timestamp before StepResult.response is rebuilt. Please merge non-null fields into the existing metadata instead of overwriting it.

@shtefcs

Copy link
Copy Markdown

Production user data point + a couple of questions on merge readiness.

We run V2 DurableAgent (@workflow/ai@5.0.0-beta.11, workflow@5.0.0-beta.20, Vercel World) for coding agents that emit large tool outputs, and #1929 is one of our top remaining payload costs. Following up on the #1929 thread (sorry for the long delay) — here's measured data from a production-preview run that should help size the win.

Decrypted doStreamStep step-output payloads from a long coding build (3 samples, decompressed):

fieldsizenote
total payload371–388 KBper doStreamStep
step.request.body347–374 KB (~96%)full input prompt — caller already has it; this PR drops it
finish~1.2 KBduplicated on step; this PR drops it
uiChunksabsentwe run collectUIMessages: false
dup tool-call lists / content / reasoningTextbytes–few KBsmall for our workload

So for our workload the redundancy this PR removes is dominated by request.body. With the recent core memoization (#2472) intentionally skipping >4 KiB string results, that ~360 KB blob is re-decrypted + re-parsed on every inline replay — roughly O(N²) over a run, tens-to-hundreds of MB of redundant work on long multi-round builds. As far as we can tell this PR is the only thing that reaches it.

Two questions before we adopt:

  1. Merge timeline — is this still on track? CI is green and mergeable: true; it reads as just pending review.
  2. Metadata concern — the review note about response-metadata chunks overwriting the whole metadata object (dropping an earlier id/timestamp) doesn't appear addressed in the current head. Is a fix planned before merge? We'd want that resolved before shipping, and would happily re-test a refreshed tarball on an isolated preview and report payload numbers back.

Thanks for the work on this — happy to provide more traces if useful.

VaguelySeriousand others added 2 commits June 24, 2026 09:53
A provider may emit multiple response-metadata chunks; a later partial
chunk must not clobber an id/timestamp set by an earlier one. Mirror the
AI SDK's `chunk.x ?? prev.x` merge so StepResult.response and the OTEL
span attributes retain every populated field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@VaguelySeriousVaguelySerious left a comment

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

AI review: no blocking issues

case 'stream-start':
warnings = chunk.warnings;
break;
case 'response-metadata':

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

AI Review: Note

This is a real behavior change, not just a refactor: the old chunksToStep did chunks.find(c => c.type === 'response-metadata') (first-wins), whereas this now merges per-field across multiple response-metadata chunks (last-non-null-wins per field). It's the correct behavior (matches the AI SDK) and the new test covers it — but for providers that split metadata across chunks it can change the reconstructed response.id / response.modelId / response.timestamp and the derived model.provider. Worth a line in the changeset/PR description so it isn't a silent diff for anyone comparing step output across versions.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(AI) Documented this behavior change in the PR description ("Behavior change worth noting"). Keeping the changeset to one terse line per repo convention.

...toolCalls.map(mapToolCall),
],
text: raw.text,
reasoning: reasoning.map((r) => ({

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

AI Review: Nit

reasoning is already materialized into its final { type: 'reasoning', text, providerOptions? } shape at the top of this function (the const reasoning = raw.reasoning.map(...) block). Re-mapping it here produces identical objects and the as SharedV3ProviderOptions cast is dead. This can collapse to just reasoning, (the earlier block already typed providerOptions correctly).

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(AI) Addressed in e4066d4 — collapsed to reuse the already-materialized reasoning array and dropped the dead cast.

} else {
// Data is a base64 string. (URL is not currently supported here —
// matches prior behavior in chunksToStep.)
const binaryString = atob(data as string);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

AI Review: Nit

RawFile.data is typed Uint8Array | string | URL, but this branch does atob(data as string) — a URL would be silently corrupted here. The behavior is carried over verbatim from the old chunksToStep (which only ever saw base64 strings), so it's not a regression, but the new explicit URL member in the type makes the gap visible. Either narrow RawFile.data to what we actually emit, or handle the URL/data-URL case (or at least guard + warn) so a provider that emits a URL-backed file doesn't produce a garbage uint8Array.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

(AI) Addressed in e4066d4RawFile.data is now derived from the V3 file stream part (string | Uint8Array), so a future widening (e.g. URL) breaks the base64-decode typecheck instead of silently corrupting bytes.

…ing remap
- Derive RawFile.data from the V3 file stream part (string | Uint8Array) so a
future widening (e.g. URL) breaks the base64-decode typecheck instead of
silently corrupting bytes.
- buildStepResult: reuse the already-materialized reasoning array instead of
re-mapping it identically; drop now-unnecessary casts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious

Copy link
Copy Markdown
MemberAuthor

(AI) Thanks for the production numbers — the request.body dominance matches the design intent. This PR moves StepResult reconstruction outside the doStreamStep step boundary, so request.body (rebuilt in workflow context from the prompt the caller already holds) no longer crosses into the durable event log. That's the ~96% field in your samples, and with #2472 skipping memoization for >4 KiB results it's exactly the blob that was re-decrypted/re-parsed on every inline replay — so this should remove that O(N²) cost for your workload.

On your two questions:

2. Metadata concern — this is addressed at the current head (e4066d4). The aggregation merges per-field rather than replacing the object, so an earlier id/timestamp survives a later partial chunk:

// packages/ai/src/agent/do-stream-step.tscase'response-metadata':
responseMetadata={id: chunk.id??responseMetadata?.id,modelId: chunk.modelId??responseMetadata?.modelId,timestamp: chunk.timestamp??responseMetadata?.timestamp,};break;

It's covered by the regression test merges partial response-metadata chunks instead of overwriting in do-stream-step.test.ts (first chunk carries id+timestamp, a later chunk carries only modelId, and the result keeps all three). If you were reading @workflow/ai@5.0.0-beta.11, that predates this PR — the merge only exists on the branch.

1. Merge timeline — I'll leave the actual scheduling to Peter, but for status: the review comments are addressed, the changeset is in, and CI is green (E2E Required Check passing). It's in a review-ready state.

If you'd like to re-test, the PR's pre-release tarballs are hosted on the docs preview:

{
"dependencies": {
"workflow": "https://workflow-docs-git-peter-issue-1929-reduce-stream-step-payload.vercel.sh/workflow.tgz",
"@workflow/ai": "https://workflow-docs-git-peter-issue-1929-reduce-stream-step-payload.vercel.sh/workflow-ai.tgz"
}
}

Refreshed payload numbers from your isolated preview would be very welcome — especially confirmation that the request.body blob is gone from the decrypted doStreamStep output.

@karthikscale3karthikscale3 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.

Checked StepResult reconstruction/public shape, response-metadata merging, raw file/reasoning aggregation, step-boundary payload/serialization concerns, changeset, and green CI.

@github-actions

Copy link
Copy Markdown
Contributor

Backport PR opened against stable: #2674. (backport job run)

Sign up for freeto 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.

DurableAgent/WorkflowAgent duplicates doStreamStep return size

3 participants

@VaguelySerious@shtefcs@karthikscale3