Skip to main content

Overview

The AI SDK integration provides Effect-first wrappers for Vercel AI SDK functions. All callbacks are automatically bridged to support Effect-based handlers, allowing you to use services and structured concurrency within AI SDK operations.

Installation

bun add ff-effect effect ai

Exports

import {
  generateText,
  streamText,
  tool,
  effectSchema,
  describe,
  AiError
} from 'ff-effect/for/ai';

generateText

Generate text using a language model with Effect-based callbacks.

Signature

function generateText<R = never>(
  params: GenerateTextParams<R>
): Effect.Effect<GenerateTextReturn, AiError, R>

Parameters

params
GenerateTextParams<R>
required
Configuration object matching AI SDK’s generateText parameters, but with callbacks accepting Effects instead of Promises.Effectified Callbacks:
  • onStepFinish?: (step) => Effect.Effect<void, never, R>
  • onFinish?: (result) => Effect.Effect<void, never, R>
  • experimental_onStart?: () => Effect.Effect<void, never, R>
  • experimental_onStepStart?: (step) => Effect.Effect<void, never, R>
  • experimental_onToolCallStart?: (call) => Effect.Effect<void, never, R>
  • experimental_onToolCallFinish?: (result) => Effect.Effect<void, never, R>
All other parameters match the AI SDK generateText API.

Returns

Returns an Effect that yields the same result type as AI SDK’s generateText:
Effect.Effect<
  {
    text: string;
    finishReason: 'stop' | 'length' | 'content-filter' | 'error' | 'other';
    usage: { promptTokens: number; completionTokens: number; totalTokens: number };
    // ... other AI SDK return fields
  },
  AiError,
  R
>

Example

import { generateText } from 'ff-effect/for/ai';
import { openai } from '@ai-sdk/openai';
import { Effect } from 'effect';

class Logger extends Effect.Service<Logger>()('Logger', {
  effect: Effect.succeed({
    info: (msg: string) => Effect.sync(() => console.log(msg))
  })
}) {}

const program = Effect.gen(function* () {
  const logger = yield* Logger;
  
  const result = yield* generateText({
    model: openai('gpt-4'),
    prompt: 'What is the meaning of life?',
    onFinish: ({ text, usage }) =>
      Effect.gen(function* () {
        yield* logger.info(`Generated: ${text}`);
        yield* logger.info(`Tokens: ${usage.totalTokens}`);
      })
  });
  
  return result.text;
}).pipe(Effect.provide(Logger.Default));

streamText

Stream text generation with Effect-based callbacks.

Signature

function streamText<R = never>(
  params: StreamTextParams<R>
): Effect.Effect<StreamTextReturn, AiError, R | Scope.Scope>

Parameters

params
StreamTextParams<R>
required
Configuration object matching AI SDK’s streamText parameters, with effectified callbacks:Effectified Callbacks:
  • onChunk?: (chunk) => Effect.Effect<void, never, R>
  • onError?: (error) => Effect.Effect<void, never, R>
  • onFinish?: (result) => Effect.Effect<void, never, R>
  • onAbort?: () => Effect.Effect<void, never, R>
  • onStepFinish?: (step) => Effect.Effect<void, never, R>
  • Plus all experimental callbacks from generateText
All other parameters match the AI SDK streamText API.

Returns

Returns an Effect that yields AI SDK’s stream text result object. Requires Scope for proper cleanup.
Always use Effect.scoped when calling streamText to ensure proper resource cleanup.

Example

import { streamText } from 'ff-effect/for/ai';
import { openai } from '@ai-sdk/openai';
import { Effect } from 'effect';

class Metrics extends Effect.Service<Metrics>()('Metrics', {
  effect: Effect.succeed({
    recordChunk: (chunk: string) => Effect.sync(() => {
      // Track streaming metrics
    })
  })
}) {}

const program = Effect.gen(function* () {
  const metrics = yield* Metrics;
  
  const stream = yield* streamText({
    model: openai('gpt-4'),
    prompt: 'Write a story about a robot',
    onChunk: ({ chunk }) =>
      Effect.gen(function* () {
        yield* metrics.recordChunk(chunk.text);
      })
  });
  
  // Use the stream
  for await (const chunk of stream.textStream) {
    console.log(chunk);
  }
}).pipe(
  Effect.scoped,  // Required for proper cleanup
  Effect.provide(Metrics.Default)
);

tool

Create AI SDK tools with Effect-based execute handlers.

Signature

function tool<INPUT, OUTPUT, R = never>(
  params: EffectToolDef<INPUT, OUTPUT, R>
): Effect.Effect<Ai.Tool<INPUT, OUTPUT>, never, R | Scope.Scope>

Parameters

params
EffectToolDef<INPUT, OUTPUT, R>
required
Tool definition object with effectified callbacks:
params.description
string
required
Human-readable description of what the tool does.
params.inputSchema
Schema<INPUT>
required
Schema for tool input validation (Zod, Valibot, or Effect schema).
params.execute
(input: INPUT, options: ToolExecutionOptions) => Effect.Effect<OUTPUT, unknown, R>
Effect-based execution function. Can access Effect services.
params.onInputStart
(options: ToolExecutionOptions) => Effect.Effect<void, never, R>
Called when input parsing starts.
params.onInputDelta
(options: { inputTextDelta: string } & ToolExecutionOptions) => Effect.Effect<void, never, R>
Called for each input text delta during streaming.
params.onInputAvailable
(options: { input: INPUT } & ToolExecutionOptions) => Effect.Effect<void, never, R>
Called when full input is available.
params.toModelOutput
(options: { toolCallId: string; input: INPUT; output: OUTPUT }) => Effect.Effect<ToolModelOutput, never, R>
Custom output formatter for the model.

Returns

Returns an Effect that yields an AI SDK Tool object. Requires Scope.

Example: Simple Tool

import { tool, effectSchema, describe } from 'ff-effect/for/ai';
import { generateText } from 'ff-effect/for/ai';
import { openai } from '@ai-sdk/openai';
import { Schema, Effect } from 'effect';

const weatherTool = yield* tool({
  description: 'Get weather for a city',
  inputSchema: effectSchema(
    Schema.Struct({
      city: Schema.String.pipe(describe('City name'))
    })
  ),
  execute: ({ city }) => Effect.succeed(`Weather in ${city}: Sunny, 72°F`)
});

const result = yield* generateText({
  model: openai('gpt-4'),
  prompt: 'What\'s the weather in San Francisco?',
  tools: { weather: weatherTool }
});

Example: Tool with Services

import { tool, effectSchema } from 'ff-effect/for/ai';
import { Schema, Effect } from 'effect';

class WeatherService extends Effect.Service<WeatherService>()("WeatherService", {
  effect: Effect.succeed({
    getWeather: (city: string) => 
      Effect.tryPromise(() => 
        fetch(`https://api.weather.com/${city}`).then(r => r.json())
      )
  })
}) {}

const program = Effect.gen(function* () {
  const weatherTool = yield* tool({
    description: 'Get real weather data',
    inputSchema: effectSchema(
      Schema.Struct({
        city: Schema.String
      })
    ),
    execute: ({ city }) =>
      Effect.gen(function* () {
        const service = yield* WeatherService;
        const data = yield* service.getWeather(city);
        return `Weather in ${city}: ${data.description}, ${data.temp}°F`;
      })
  });
  
  return weatherTool;
}).pipe(
  Effect.scoped,
  Effect.provide(WeatherService.Default)
);

effectSchema

Convert an Effect Schema to an AI SDK schema with automatic validation.

Signature

function effectSchema<A, I>(
  schema: Schema.Schema<A, I>
): FlexibleSchema<A>

Example

import { effectSchema, describe } from 'ff-effect/for/ai';
import { Schema } from 'effect';

const userSchema = effectSchema(
  Schema.Struct({
    name: Schema.String.pipe(describe('User full name')),
    age: Schema.Number.pipe(describe('User age in years')),
    email: Schema.String.pipe(describe('User email address'))
  })
);

const result = yield* generateText({
  model: openai('gpt-4'),
  prompt: 'Extract user info from: John Doe, 30 years old, john@example.com',
  output: 'object',
  schema: userSchema
});

console.log(result.object); // { name: 'John Doe', age: 30, email: 'john@example.com' }

describe

Add descriptions to Effect Schema fields for better AI understanding.

Signature

function describe(
  description: string
): <A, I, R>(schema: Schema.Schema<A, I, R>) => Schema.Schema<A, I, R>

Example

import { describe } from 'ff-effect/for/ai';
import { Schema } from 'effect';

const PersonSchema = Schema.Struct({
  name: Schema.String.pipe(
    describe('Full name of the person')
  ),
  age: Schema.Number.pipe(
    describe('Age in years, must be positive')
  ),
  occupation: Schema.optional(
    Schema.String.pipe(
      describe('Current job title or profession')
    )
  )
});

AiError

Tagged error type for AI SDK operation failures.
class AiError extends Data.TaggedError('ff-effect/AiError')<{
  message: string;
  cause?: unknown;
}> {}

Example Error Handling

import { generateText, AiError } from 'ff-effect/for/ai';
import { Effect } from 'effect';

const program = Effect.gen(function* () {
  const result = yield* generateText({
    model: openai('gpt-4'),
    prompt: 'Hello!'
  }).pipe(
    Effect.catchTag('ff-effect/AiError', (error) =>
      Effect.gen(function* () {
        console.error('AI SDK Error:', error.message);
        console.error('Cause:', error.cause);
        return yield* Effect.fail(error);
      })
    )
  );
  
  return result;
});

Complete Example

import { generateText, tool, effectSchema, describe } from 'ff-effect/for/ai';
import { openai } from '@ai-sdk/openai';
import { Schema, Effect } from 'effect';

class Database extends Effect.Service<Database>()('Database', {
  effect: Effect.succeed({
    getUser: (id: string) => Effect.succeed({ id, name: 'Alice', age: 30 })
  })
}) {}

class Logger extends Effect.Service<Logger>()('Logger', {
  effect: Effect.succeed({
    info: (msg: string) => Effect.sync(() => console.log(`[INFO] ${msg}`))
  })
}) {}

const program = Effect.gen(function* () {
  const logger = yield* Logger;
  
  // Create a tool that uses services
  const getUserTool = yield* tool({
    description: 'Get user information by ID',
    inputSchema: effectSchema(
      Schema.Struct({
        userId: Schema.String.pipe(describe('The user ID to look up'))
      })
    ),
    execute: ({ userId }) =>
      Effect.gen(function* () {
        const db = yield* Database;
        const user = yield* db.getUser(userId);
        yield* logger.info(`Fetched user: ${user.name}`);
        return `User ${user.name} is ${user.age} years old`;
      })
  });
  
  // Generate text with the tool
  const result = yield* generateText({
    model: openai('gpt-4'),
    prompt: 'Get information for user ID "123"',
    tools: { getUser: getUserTool },
    onFinish: ({ usage }) =>
      Effect.gen(function* () {
        yield* logger.info(`Tokens used: ${usage.totalTokens}`);
      })
  });
  
  return result.text;
}).pipe(
  Effect.scoped,
  Effect.provide(Database.Default),
  Effect.provide(Logger.Default)
);

Effect.runPromise(program).then(console.log);

See Also