AI SDK Integration
How the default workflowHarness uses the AI SDK internally — generateText/streamText, Output.* mapping, tool interop, and durability.
Little Workflow: published on npm as little-workflow@0.2.0-alpha.1 (2026-10-06). Sections labeled UNRELEASED describe source-only changes. Release status
The alpha SDK integrates with the AI SDK in one place: the default workflowHarness implementation in packages/little-harness/src/workflow-harness/workflow-harness.ts. The prior RuntimeAiAdapter and PlannerAdapter interfaces are gone — worker execution for all step types is unified through the harness (see Harness). The AI SDK's tool({...}) from the "ai" package is now the canonical tool primitive — import it directly and pass it to createToolRegistry.
This page covers how workflowHarness uses the AI SDK under the hood.
What Changed
| Prior alpha | This milestone |
|---|---|
RuntimeAiAdapter.generate(options) — custom interface for AI step execution | Worker harness dispatches ai.generate through workflowHarness and the AI SDK module supplied to it |
createRuntimeAiAdapter(ai) — factory passed to runWorkflow as the ai option | Gone. runWorkflow no longer accepts ai or codeRunner options. |
createAiSdkPlannerAdapter(ai, { model, system }) — function-only planner | Gone. Planner lives in a harness session; WorkflowDefinition.planner.harness is the config point. |
Custom ToolDescriptor shape for tool registration | AI SDK's tool({...}) — import from "ai", use directly. |
code.run signature async (input) => output | async ({ input, tools }) => output — tools proxy injected by the harness. |
workflowHarness — AI SDK Usage
workflowHarness runs a small agent loop around the AI SDK module. When streamText is present it is used and the terminal promise fields are drained; otherwise the harness falls back to generateText. Both paths receive the same AI SDK 7 request shape: model, instructions (the role's system prompt), messages, tools, abort signal, and an AI SDK output helper.
// packages/little-harness/src/workflow-harness/workflow-harness.ts (simplified)
export const workflowHarness: Harness = {
async run(task, ctx) {
switch (task.kind) {
case "plan":
return runPlanTask(task, ctx);
case "orchestrate":
return runOrchestrateTask(task, ctx);
case "execute_step":
return runExecuteStepTask(task, ctx); // dispatches by step.uses
case "fix_step":
return runFixStepTask(task, ctx);
}
},
};The agent-loop tasks configure the AI SDK request as follows:
- System prompt assembly:
ctx.system(dev-supplied) + role-specific addendum + skill frontmatter list + memory/scratch mount descriptions + available tool descriptions. - Tool exposure: passes
ctx.tools(pre-merged by the SDK; includes role-specific injected tools) as model-facing AI SDK tools. - Per-turn event recording: before each model invocation, emits
harness.model.called; after the response, emitsharness.model.responded. Per tool call, emitsharness.tool_call.started(caller: "model")+harness.tool_call.succeeded, harness.tool_call.failed. - Durability checks: before invoking the model, checks the event log for an existing committed
harness.model.respondedmatching this turn'spromptHash— if present, skips the model call and replays the cached response. Same for tool calls. - Memory + scratch mounts: ensures dirs exist; exposes them to the bash tool's cwd-allowlist.
An ai.generate step is not an agent loop: it is one model call with no tools. instructions is the worker's system prompt and skill list followed by the step's with.system; messages is one user message, with.prompt + "\n\nInput:\n" + the resolved step input; tools is empty, so the workflow's tools and bash are not offered. The runtime resolves {{ ... }} expressions in with.prompt and with.system before the harness builds the request.
Output.* Mapping
For ai.generate steps, the step's LWIR output contract drives how the AI SDK call is structured:
LWIR output.mode | AI SDK call |
|---|---|
text | Output.text() |
object | Output.object({ schema }) |
array | Output.object({ schema }) with the full array schema |
choice | Output.choice({ options: values }) |
json | Output.json({}) (schema omitted) |
aiSdkOutputSpec() picks the helper for the step's mode. Array output intentionally uses Output.object({ schema }) with the complete array schema rather than Output.array(), because the latter wraps as { elements: T[] } for providers that expect a top-level object.
The harness reads structured output from result.output or result.object, and falls back to result.text for text-mode output.
AI SDK Module Requirements
The default workflowHarness accepts an injected aiSdkModule so tests and custom hosts can provide the exact AI SDK instance they want. The module must expose generateText and the Output helpers used by the selected output modes. streamText is optional.
The older generateObject path is not supported by the default harness. If a host needs a different provider contract, it should implement Harness.run(task, ctx) directly.
Tool Interop
Tools are declared with AI SDK's tool({...}) and registered in a ToolRegistry. The harness receives ctx.tools as executable tools, then builds a model-facing tool map before the AI SDK call: executable handlers are kept in the harness, descriptions and input schemas are exposed to the model, and plain JSON schemas are wrapped with jsonSchema() when the installed AI SDK module provides it.
// Dev declares:
import { tool } from "ai";
import { z } from "zod";
const fetchCandidates = tool({
description: "Fetch candidates from ATS by job id.",
inputSchema: z.object({ jobId: z.string() }),
execute: async ({ jobId }) => { /* ... */ },
});
// Registered via:
const tools = createToolRegistry({ fetchCandidates });
// Passed to runWorkflow; the harness receives them via ctx.tools.The AI SDK tool format is now the canonical shape. Existing code using Little Workflow's prior custom ToolDescriptor format needs migration — wrap with tool({...}) from "ai" and pass inputSchema as a Zod schema.
MCP Tools
Declarative MCP belongs on runWorkflow(workflow, input, { mcp }); Little Workflow resolves those servers through Little Harness gateway tools. Use manual MCP attachment only when the caller already owns MCP client lifetime. The client comes from AI SDK 7's companion package @ai-sdk/mcp (install separately: pnpm add @ai-sdk/mcp):
import { createMCPClient } from "@ai-sdk/mcp";
const client = await createMCPClient({ transport: /* ... */ });
const mcpTools = await client.tools();
tools.attachMcpTools(mcpTools, { prefix: "atlas" });MCP-derived tools and declarative gateway tools use the same tool({...}) execution path as native tools. Harness tool events record their invocations with caller: "model" when invoked from an agent loop.
Cancellation And Usage
Every model turn builds one AI SDK request and records the response in the harness event log:
- Builds a base request (model,
instructions, messages, tools,abortSignal, andoutput). - Calls
streamText(request)when available, otherwisegenerateText(request). - Coerces the response into the harness's internal result shape with
output,text,reasoning,toolCalls, andusage.
A failed call throws the provider's own error, which is recorded as harness.model.failed: the harness passes streamText an onError that keeps the error (and keeps it off the console), and a rejected text or finishReason also throws. Only output is read leniently, because it also rejects for a turn that ends in tool calls. In a planner, fixer or orchestrator loop, a turn with tool calls runs them and calls the model again; the loop ends on the first turn with no tool calls.
Usage normalization handles both AI SDK naming styles (inputTokens/promptTokens, outputTokens/completionTokens). In AI SDK 7, usage is the all-steps total, so it is read first; totalUsage is read only from older, v6-shaped results that lack usage. It also carries through two subset counts, reading whichever spelling the provider used:
cachedInputTokens— from AI SDK 7'sinputTokenDetails.cacheReadTokens, thenusage.cachedInputTokens,inputTokenDetails.cachedTokens, orpromptTokensDetails.cachedTokens. These are prompt-cache hits, a subset ofinputTokens, not an addition to it.reasoningTokens— fromoutputTokenDetails.reasoningTokens,usage.reasoningTokens, orcompletionTokensDetails.reasoningTokens. A subset ofoutputTokens, billed at the normal output rate and therefore never charged twice.
Usage lands in harness.model.responded.payload.response.usage and is rolled up per session into harness.session.completed.payload.usage. Both are token-only: this package cannot see the model registry — it lives in @little-stack/workflow, which depends on the harness — so a cost computed here could only ever be a stub. @little-stack/workflow's event recorder rejects a costUsd on any recorded usage payload, which makes the wrong design unrepresentable rather than merely discouraged.
What the harness does contribute to pricing is identity: harness.model.responded.payload.model records { provider, modelId } on the payload (not just the trace metadata) so the downstream materializer has a registry key to price against. See Event Log → Pricing.
The ctx.abortSignal is forwarded to the AI SDK request and to tool execute() calls, both as the abortSignal execute option and as signal on the AI SDK 7 context option (formerly experimental_context).
Custom Harnesses
If you need a different runtime (e.g., Cloudflare Workers, a different agent loop library), implement the Harness contract directly rather than wrapping workflowHarness:
import type { Harness } from "@little-stack/workflow/internals";
export const customHarness: Harness = {
async run(task, ctx) {
// Implement agent loop using your preferred library.
// Must emit harness.session.*, harness.model.*, and harness.tool_call.* events
// via ctx.recorder to preserve replay + audit semantics.
// Must honor ctx.abortSignal for cancellation.
},
};The harness contract is in harness/types.ts. workflowHarness is one implementation; the rest of the SDK doesn't depend on it directly.
Why The Bridge Lives In One Place
Keeping all AI SDK contact in packages/little-harness/src/workflow-harness/workflow-harness.ts gets you:
- One place to support a new AI SDK version — feature detection, new
Output.*helpers, new agent primitives. - One place to add streaming. The harness internally streams from AI SDK; exposing that stream to
runWorkflowcallers is a post-alpha surface. - One place to mock for tests. Every test that needs deterministic LLM output supplies a stub
Harness(conforming to theHarnessinterface) rather than patching AI SDK internals.