Skip to content

Reference

Decision models

Use Decision and DecisionModel from effect/ai to evaluate schema-defined input. The model returns evidence; application code owns thresholds, routing, and side effects. Start with the decision guide or the complete example.

Decision.make({ input, decisions }) pairs an input Schema with named decisions. DecisionModel.decide(definition, { input }) encodes that input using Schema.toCodecJson and answers all decisions in one provider call. Encoding services remain visible in the Effect’s requirements. Include only data the provider should receive.

Constructor Required options besides string instructions Answer
Decision.classify criteria: at least two labels mapped to string descriptions label, probabilities, optional confidence
Decision.rate criteria: at least two distinct ordered string levels rating, label, probabilities, optional confidence
Decision.probability None; optional criteria describes both false and true probability

Classification labels and rating levels infer literal unions. Ratings can be fractional, from zero to the last level’s index; their probability keys are the level strings. The rating label is the most probable level, choosing the first on ties. Probability answers are in [0, 1]. Probability criteria are optional; when supplied, describe both outcomes. Code inspecting a Decision.Probability must check whether criteria is present before reading it. Constructors throw for empty decision sets or invalid criteria counts; define valid static assessments before executing them.

import {
import Schema
Schema
} from "effect";
import {
import Decision
Decision
,
import DecisionModel
DecisionModel
} from "effect/ai";
const
const Urgency: Decision.Definition<Schema.Struct<{
readonly message: Schema.String;
}>, {
urgent: Decision.Probability;
}>
Urgency
=
import Decision
Decision
.
const make: <Schema.Struct<{
readonly message: Schema.String;
}>, {
urgent: Decision.Probability;
}>(options: {
readonly input: Schema.Struct<{
readonly message: Schema.String;
}>;
readonly decisions: {
urgent: Decision.Probability;
};
}) => Decision.Definition<Schema.Struct<{
readonly message: Schema.String;
}>, {
urgent: Decision.Probability;
}>

Creates a decision definition from an input schema and named decisions. DecisionModel.decide encodes the input with Schema.toCodecJson before calling the provider. Answer keys and types are inferred from the decisions. Throws if decisions is empty.

Example (Defining ticket triage)

import { Schema } from "effect"
import { Decision } from "effect/ai"
const Ticket = Schema.Struct({
subject: Schema.String,
body: Schema.String
})
const TicketTriage = Decision.make({
input: Ticket,
decisions: {
department: Decision.classify({
instructions: "Which team should handle this",
criteria: { billing: "payments", technical: "bugs" }
}),
urgent: Decision.probability({
instructions: "The message is time-sensitive",
criteria: { false: "No time pressure", true: "Needs action now" }
})
}
})

@see ― Definition for the returned shape

@stability ― unstable

@category ― constructors

@since ― 4.0.0

make
({
input: Schema.Struct<{
readonly message: Schema.String;
}>
input
:
import Schema
Schema
.
function Struct<{
readonly message: Schema.String;
}>(fields: {
readonly message: Schema.String;
}): Schema.Struct<{
readonly message: Schema.String;
}>

Defines a struct schema from a map of field schemas.

Details

Each field value is a schema. Use

optionalKey

or

optional

to mark fields as optional, and

mutableKey

to mark them as mutable.

The resulting schema's Type is a readonly object type with the fields' decoded types. The Encoded form mirrors the field schemas' encoded types. Declared fields may be inherited and are copied to own properties in the output. The __proto__ field is accepted only when it is an own property. Parsing does not guarantee that output keys retain their input order.

Example (Defining a basic struct)

import { Schema } from "effect"
const Person = Schema.Struct({
name: Schema.String,
age: Schema.Number,
email: Schema.optionalKey(Schema.String)
})
// { readonly name: string; readonly age: number; readonly email?: string }
type Person = typeof Person.Type
Schema.decodeUnknownSync(Person)({ name: "Alice", age: 30 }) // => { name: "Alice", age: 30 }

@category ― constructors

@since ― 3.10.0

Struct
({
message: Schema.String
message
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@category ― models

@since ― 4.0.0

@category ― schemas

@since ― 4.0.0

String
}),
decisions: {
urgent: Decision.Probability;
}
decisions
: {
urgent: Decision.Probability
urgent
:
import Decision
Decision
.
const probability: (options: {
readonly instructions: string;
readonly criteria?: {
readonly false: string;
readonly true: string;
} | undefined;
}) => Decision.Probability

Creates a probability decision from instructions and optional outcome descriptions. When criteria is supplied, descriptions for both false and true are required.

Example (Estimating urgency)

import { Decision } from "effect/ai"
const urgent = Decision.probability({
instructions: "The message is time-sensitive"
})

Example (Providing outcome descriptions)

import { Decision } from "effect/ai"
const urgent = Decision.probability({
instructions: "The message is time-sensitive",
criteria: {
false: "The message can wait",
true: "The message needs immediate attention"
}
})

@see ― classify for more than two outcomes

@stability ― unstable

@category ― constructors

@since ― 4.0.0

probability
({
instructions: string
instructions
: "Does this need immediate attention?",
}),
},
});
const
const assessment: Effect<DecisionModel.DecideResponse<{
urgent: Decision.Probability;
}>, AiError, DecisionModel.DecisionModel>
assessment
=
import DecisionModel
DecisionModel
.
const decide: <Schema.Struct<{
readonly message: Schema.String;
}>, {
urgent: Decision.Probability;
}>(definition: Decision.Definition<Schema.Struct<{
readonly message: Schema.String;
}>, {
urgent: Decision.Probability;
}>, options: DecisionModel.DecideOptions<Schema.Struct<{
readonly message: Schema.String;
}>>) => Effect<DecisionModel.DecideResponse<{
urgent: Decision.Probability;
}>, AiError, DecisionModel.DecisionModel>

Answers a decision definition using the current DecisionModel service. Encodes the input with Schema.toCodecJson, requiring the schema's encoding services. Explicit undefined fields become null; absent fields stay absent. Custom declarations need a JSON codec annotation or encoding fails. Returned answers and probability dictionaries have null prototypes.

Example (Triaging a ticket)

import { Effect, Schema } from "effect"
import { Decision, DecisionModel } from "effect/ai"
const TicketTriage = Decision.make({
input: Schema.String,
decisions: {
urgent: Decision.probability({
instructions: "The message is time-sensitive",
criteria: { false: "No time pressure", true: "Needs action now" }
})
}
})
const program = Effect.gen(function*() {
const { answers, usage } = yield* DecisionModel.decide(TicketTriage, {
input: "My card was charged twice, please fix this today"
})
return { probability: answers.urgent.probability, usage }
})

@see ― DecisionModel for the service this function requires

@stability ― unstable

@category ― decisions

@since ― 4.0.0

decide
(
const Urgency: Decision.Definition<Schema.Struct<{
readonly message: Schema.String;
}>, {
urgent: Decision.Probability;
}>
Urgency
, {
DecideOptions<Struct<{ readonly message: String; }>>.input: {
readonly message: string;
}
input
: {
message: string
message
: "Our production deployment is blocked." },
});

Results contain answers keyed by decision name and usage.inputTokens / usage.outputTokens. Unreported token counts are undefined. Usage is separate from an agent Run’s language-model budgets. Classification and rating confidence is optional, provider-defined evidence, not a correctness guarantee. Provider and resolved model identifiers are not part of the response.

Native DecisionModel validates required answers, kinds, labels, finite probabilities in [0, 1], and, by default, distribution sums within 1e-6 of 1. Classification may return a label that does not have the highest probability. Ratings must lie within the scale; the core derives their labels from the distribution. Application acceptance policies remain explicit.

Providers can opt into probabilityPrecision to accept rounding drift and rescale distributions. TypeSafe sets this to two decimal places, so rounded totals such as 0.99 or 1.01 can be accepted. The language model adapter keeps strict validation; invalid sums fail with AiError.InvalidOutputError.

Provide LanguageModelDecisionModel.layer with any native language model that supports structured output.

import {
import LanguageModelDecisionModel
LanguageModelDecisionModel
} from "@yielded/agent-ai-decision";
import {
import OpenAiClient
OpenAiClient
,
import OpenAiLanguageModel
OpenAiLanguageModel
} from "@effect/ai-openai";
import {
import Config
Config
,
import Effect
Effect
,
import Layer
Layer
,
import Schema
Schema
} from "effect";
import {
import Decision
Decision
,
import DecisionModel
DecisionModel
} from "effect/ai";
import {
import FetchHttpClient
FetchHttpClient
} from "effect/http";
const
const Sentiment: Decision.Definition<Schema.String, {
tone: Decision.Classify<"positive" | "negative">;
}>
Sentiment
=
import Decision
Decision
.
const make: <Schema.String, {
tone: Decision.Classify<"positive" | "negative">;
}>(options: {
readonly input: Schema.String;
readonly decisions: {
tone: Decision.Classify<"positive" | "negative">;
};
}) => Decision.Definition<Schema.String, {
tone: Decision.Classify<"positive" | "negative">;
}>

Creates a decision definition from an input schema and named decisions. DecisionModel.decide encodes the input with Schema.toCodecJson before calling the provider. Answer keys and types are inferred from the decisions. Throws if decisions is empty.

Example (Defining ticket triage)

import { Schema } from "effect"
import { Decision } from "effect/ai"
const Ticket = Schema.Struct({
subject: Schema.String,
body: Schema.String
})
const TicketTriage = Decision.make({
input: Ticket,
decisions: {
department: Decision.classify({
instructions: "Which team should handle this",
criteria: { billing: "payments", technical: "bugs" }
}),
urgent: Decision.probability({
instructions: "The message is time-sensitive",
criteria: { false: "No time pressure", true: "Needs action now" }
})
}
})

@see ― Definition for the returned shape

@stability ― unstable

@category ― constructors

@since ― 4.0.0

make
({
input: Schema.String
input
:
import Schema
Schema
.
const String: Schema.String

Type-level representation of

String

.

Schema for string values. Validates that the input is typeof "string".

@category ― models

@since ― 4.0.0

@category ― schemas

@since ― 4.0.0

String
,
decisions: {
tone: Decision.Classify<"positive" | "negative">;
}
decisions
: {
tone: Decision.Classify<"positive" | "negative">
tone
:
import Decision
Decision
.
const classify: <"positive" | "negative">(options: {
readonly instructions: string;
readonly criteria: {
readonly positive: string;
readonly negative: string;
};
}) => Decision.Classify<"positive" | "negative">

Creates a classification decision from labelled criteria. Throws if fewer than two labels are supplied.

Example (Choosing a department)

import { Decision } from "effect/ai"
const department = Decision.classify({
instructions: "Which team should handle this",
criteria: {
billing: "payments",
technical: "bugs",
sales: "pricing"
}
})

@see ― rate for an ordered scale

@see ― probability for a single yes or no likelihood

@stability ― unstable

@category ― constructors

@since ― 4.0.0

classify
({
instructions: string
instructions
: "Classify the sentiment.",
criteria: {
readonly positive: string;
readonly negative: string;
}
criteria
: {
positive: string
positive
: "Expresses satisfaction",
negative: string
negative
: "Expresses dissatisfaction",
},
}),
},
});
const
const DecisionLive: Layer.Layer<DecisionModel.DecisionModel, Config.ConfigError, never>
DecisionLive
=
import LanguageModelDecisionModel
LanguageModelDecisionModel
.
const layer: Layer.Layer<DecisionModel.DecisionModel, never, LanguageModel>

Adapts the supplied LanguageModel to native Effect decisions using structured output.

Requires a model supporting generateObject. All decisions share one request; native DecisionModel validation rejects invalid distributions without normalization or retry. Probabilities are LLM estimates, not calibrated confidence scores. Provider errors, defects, and interruption propagate; callers own retry, timeout, and fallback policies.

layer
.
Pipeable.pipe<Layer.Layer<DecisionModel.DecisionModel, never, LanguageModel>, Layer.Layer<DecisionModel.DecisionModel, never, OpenAiClient.OpenAiClient>, Layer.Layer<DecisionModel.DecisionModel, Config.ConfigError, HttpClient>, Layer.Layer<DecisionModel.DecisionModel, Config.ConfigError, never>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<DecisionModel.DecisionModel, never, LanguageModel>) => Layer.Layer<...>, bc: (_: Layer.Layer<...>) => Layer.Layer<...>, cd: (_: Layer.Layer<...>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <OpenAiClient.OpenAiClient, never, LanguageModel | ProviderName | ModelName>(that: Layer.Layer<LanguageModel | ProviderName | ModelName, never, OpenAiClient.OpenAiClient>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, OpenAiClient.OpenAiClient | Exclude<RIn2, LanguageModel | ProviderName | ModelName>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
import OpenAiLanguageModel
OpenAiLanguageModel
.
const model: (model: (string & {}) | OpenAiLanguageModel.Model, config?: Omit<{
readonly metadata?: {
readonly [x: string]: string;
} | undefined;
readonly top_logprobs?: number | undefined;
readonly temperature?: number | undefined;
readonly top_p?: number | undefined;
readonly user?: string | undefined;
readonly prompt_cache_key?: string | undefined;
readonly prompt_cache_options?: {
readonly mode?: "explicit" | "implicit" | undefined;
readonly ttl?: "30m" | undefined;
} | undefined;
... 17 more ...;
readonly useItemReferences?: boolean | undefined;
}, "model">) => Model<"openai", LanguageModel, OpenAiClient.OpenAiClient>

Creates an OpenAI model descriptor that can be provided with Effect.provide.

When to use

Use when you want an OpenAI language model value that carries provider and model metadata and can be supplied directly to an Effect program.

@see ― layer for creating a LanguageModel.LanguageModel layer directly

@see ― make for constructing the language model service effectfully

@stability ― unstable

@category ― constructors

@since ― 4.0.0

model
("gpt-6-luna", {
service_tier?: string | undefined
service_tier
: "priority" })),
import Layer
Layer
.
const provide: <HttpClient, Config.ConfigError, OpenAiClient.OpenAiClient>(that: Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, HttpClient>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, Config.ConfigError | E2, HttpClient | Exclude<RIn2, OpenAiClient.OpenAiClient>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
import OpenAiClient
OpenAiClient
.
const layerConfig: (options?: {
readonly apiKey?: Config.Config<Redacted<string> | undefined> | undefined;
readonly apiUrl?: Config.Config<string> | undefined;
readonly organizationId?: Config.Config<Redacted<string> | undefined> | undefined;
readonly projectId?: Config.Config<Redacted<string> | undefined> | undefined;
readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined;
}) => Layer.Layer<OpenAiClient.OpenAiClient, Config.ConfigError, HttpClient>

Creates a layer for the OpenAI client from provided Config values.

When to use

Use when you need client settings for OpenAI-compatible APIs to be read from Effect Config values while providing OpenAiClient as a Layer.

Details

Only config values supplied in options are loaded. Omitted fields are passed to make as undefined, and transformClient is forwarded as a plain option.

@see ― make for constructing the client service effectfully

@see ― layer for providing the client from already-resolved options

@stability ― unstable

@category ― layers

@since ― 4.0.0

layerConfig
({
apiKey?: Config.Config<Redacted<string> | undefined> | undefined

The config value to load for the API key.

apiKey
:
import Config
Config
.
function Redacted(name?: string): Config.Config<import("effect/Redacted").Redacted<string>>

Creates a config for a redacted string value. The parsed result is wrapped in a Redacted container that hides the value from logs and toString.

When to use

Use to read secret string settings that should not be exposed in logs or string output.

Details

Shortcut for Config.schema(Schema.Redacted(Schema.String), name).

Example (Reading a secret)

import { Config, ConfigProvider, Effect } from "effect"
const program = Config.Redacted("API_KEY").pipe(Effect.map(String))
const provider = ConfigProvider.fromEnv({
env: {
API_KEY: "sk-1234567890abcdef"
}
})
Effect.runSync(
program.pipe(Effect.provideService(ConfigProvider.ConfigProvider, provider))
) // => "<redacted>"

@see ― String for non-secret string settings

@category ― constructors

@since ― 2.0.0

Redacted
("OPENAI_API_KEY") })),
import Layer
Layer
.
const provide: <never, never, HttpClient>(that: Layer.Layer<HttpClient, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, Exclude<RIn2, HttpClient>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
import FetchHttpClient
FetchHttpClient
.
const layer: Layer.Layer<HttpClient, never, never>

Layer that provides an HttpClient implementation backed by the configured Fetch function.

When to use

Use when an Effect program should execute HttpClient requests through the platform fetch implementation, especially in browser, edge, or Node.js runtimes with globalThis.fetch.

Details

The layer uses the current Fetch reference and optional RequestInit service for each request. Request-specific method, headers, body, and abort signal are supplied by the client and override matching RequestInit fields.

Gotchas

Fetch behavior comes from the runtime's implementation, so CORS, cookies, redirects, abort handling, and streaming support can vary by platform. Stream request bodies are sent as Web streams with duplex: "half", and any content-length header is removed before calling fetch.

@see ― Fetch for supplying the fetch implementation used by this layer

@see ― RequestInit for default RequestInit options applied before request-specific fields

@stability ― unstable

@category ― layers

@since ― 4.0.0

layer
),
);
const
const program: Effect.Effect<"positive" | "negative", AiError | Config.ConfigError, never>
program
=
import DecisionModel
DecisionModel
.
const decide: <Schema.String, {
tone: Decision.Classify<"positive" | "negative">;
}>(definition: Decision.Definition<Schema.String, {
tone: Decision.Classify<"positive" | "negative">;
}>, options: DecisionModel.DecideOptions<Schema.String>) => Effect.Effect<DecisionModel.DecideResponse<{
tone: Decision.Classify<"positive" | "negative">;
}>, AiError, DecisionModel.DecisionModel>

Answers a decision definition using the current DecisionModel service. Encodes the input with Schema.toCodecJson, requiring the schema's encoding services. Explicit undefined fields become null; absent fields stay absent. Custom declarations need a JSON codec annotation or encoding fails. Returned answers and probability dictionaries have null prototypes.

Example (Triaging a ticket)

import { Effect, Schema } from "effect"
import { Decision, DecisionModel } from "effect/ai"
const TicketTriage = Decision.make({
input: Schema.String,
decisions: {
urgent: Decision.probability({
instructions: "The message is time-sensitive",
criteria: { false: "No time pressure", true: "Needs action now" }
})
}
})
const program = Effect.gen(function*() {
const { answers, usage } = yield* DecisionModel.decide(TicketTriage, {
input: "My card was charged twice, please fix this today"
})
return { probability: answers.urgent.probability, usage }
})

@see ― DecisionModel for the service this function requires

@stability ― unstable

@category ― decisions

@since ― 4.0.0

decide
(
const Sentiment: Decision.Definition<Schema.String, {
tone: Decision.Classify<"positive" | "negative">;
}>
Sentiment
, {
DecideOptions<String>.input: string
input
: "This is excellent!" }).
Pipeable.pipe<Effect.Effect<DecisionModel.DecideResponse<{
tone: Decision.Classify<"positive" | "negative">;
}>, AiError, DecisionModel.DecisionModel>, Effect.Effect<"positive" | "negative", AiError, DecisionModel.DecisionModel>, Effect.Effect<"positive" | "negative", AiError | Config.ConfigError, never>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<DecisionModel.DecideResponse<{
tone: Decision.Classify<"positive" | "negative">;
}>, AiError, DecisionModel.DecisionModel>) => Effect.Effect<...>, bc: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const map: <DecisionModel.DecideResponse<{
tone: Decision.Classify<"positive" | "negative">;
}>, "positive" | "negative">(f: (a: DecisionModel.DecideResponse<{
tone: Decision.Classify<"positive" | "negative">;
}>) => "positive" | "negative") => <E, R>(self: Effect.Effect<DecisionModel.DecideResponse<{
tone: Decision.Classify<"positive" | "negative">;
}>, E, R>) => Effect.Effect<"positive" | "negative", E, R> (+1 overload)

Transforms the value inside an effect by applying a function to it.

When to use

Use to transform an effect's success value with a function that returns a plain value, producing a new effect without changing the original effect's typed error or context requirements.

Details

map takes a function and applies it to the value contained within an effect, creating a new effect with the transformed value.

It's important to note that effects are immutable, meaning that the original effect is not modified. Instead, a new effect is returned with the updated value.

Example (Choosing map syntax variants)

import { Effect, pipe } from "effect"
const output: Array<unknown> = []
const myEffect = Effect.succeed(1)
const transformation = (n: number) => n + 1
const mappedWithPipe = pipe(myEffect, Effect.map(transformation))
const mappedWithDataFirst = Effect.map(myEffect, transformation)
const mappedWithMethod = myEffect.pipe(Effect.map(transformation))
void output.push(Effect.runSync(Effect.all([
mappedWithPipe,
mappedWithDataFirst,
mappedWithMethod
])))
output // => [[2, 2, 2]]

Example (Adding a service charge)

import { Effect, pipe } from "effect"
const addServiceCharge = (amount: number) => amount + 1
const fetchTransactionAmount = Effect.promise(() => Promise.resolve(100))
const finalAmount = pipe(
fetchTransactionAmount,
Effect.map(addServiceCharge)
)
await Effect.runPromise(finalAmount) // => 101

@see ― mapError for a version that operates on the error channel.

@see ― mapBoth for a version that operates on both channels.

@see ― flatMap or andThen for a version that can return a new effect.

@category ― mapping

@since ― 2.0.0

map
(({
answers: Decision.Answers<{
tone: Decision.Classify<"positive" | "negative">;
}>
answers
}) =>
answers: Decision.Answers<{
tone: Decision.Classify<"positive" | "negative">;
}>
answers
.
tone: Decision.ClassifyAnswer<"positive" | "negative">
tone
.
ClassifyAnswer<"positive" | "negative">.label: "positive" | "negative"
label
), // "positive" | "negative"
import Effect
Effect
.
const provide: <DecisionModel.DecisionModel, Config.ConfigError, never>(layer: Layer.Layer<DecisionModel.DecisionModel, Config.ConfigError, never>, options?: {
readonly local?: boolean | undefined;
} | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, Config.ConfigError | E, Exclude<R, DecisionModel.DecisionModel>> (+5 overloads)

Provides dependencies to an effect using layers or a context. Use options.local to build the layer every time; by default, layers are shared between provide calls.

Example (Providing dependencies with a layer)

import { Context, Effect, Layer } from "effect"
interface Database {
readonly query: (sql: string) => Effect.Effect<string>
}
const Database = Context.Service<Database>("Database")
const DatabaseLayer = Layer.succeed(Database)({
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))
})
const program = Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
const provided = Effect.provide(program, DatabaseLayer)
await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"

@category ― providing services

@since ― 2.0.0

provide
(
const DecisionLive: Layer.Layer<DecisionModel.DecisionModel, Config.ConfigError, never>
DecisionLive
),
);

The adapter answers all classification, rating, and probability decisions in one generateObject call. It derives the response schema from the decisions, puts decision instructions in the system message, and sends schema-encoded input as untrusted user data. Prompt separation does not make model decisions an authorization boundary. Input is still sent to the selected provider.

Probabilities are LLM estimates, not calibrated confidence scores. Native DecisionModel validation applies unchanged: invalid distributions fail with InvalidOutputError without normalization. JSON or schema decoding failures retain StructuredOutputError. Token usage is forwarded; the adapter does not invent confidence values. Provider errors, defects, and interruption propagate. Configure model options on the supplied Layer and compose retry, timeout, or failover policies explicitly; this adapter does not automatically retry another provider.

The Layer requires LanguageModel and provides DecisionModel. Import it from the package root or @yielded/agent-ai-decision/language-model-decision-model. The provider-neutral example uses the same decision API without selecting a provider.

AutoModel is a native model Layer: provide it to satisfy an agent’s model requirement. The runtime selects before each thread’s first model call and keeps that choice for later turns and follow-up runs. Each new subagent selects independently from its delegated task.

auto-model.ts
export const
const ThreadModels: AutoModel.AutoModel<OpenAiClient.OpenAiClient>
ThreadModels
=
import AutoModel
AutoModel
.
const make: <{
routine: OpenAiClient.OpenAiClient;
complex: OpenAiClient.OpenAiClient;
}>(options: {
readonly models: {
readonly routine: AutoModel.Candidate<OpenAiClient.OpenAiClient>;
readonly complex: AutoModel.Candidate<OpenAiClient.OpenAiClient>;
};
readonly version: string;
readonly instructions?: string;
}) => AutoModel.AutoModel<OpenAiClient.OpenAiClient>

Describe at least two approved native models and evaluate them through DecisionModel. Supply Jev with TypeSafeDecisionModel.model("jev-latest") from

@effect ―

/ai-typesafe. Increment version whenever a profile's model, effort, or other settings change; retain old catalogs while their threads remain active.

State should describe the whole task, relevant context, constraints, and tools; only include information the decision provider may receive. The application must filter candidates for authorization and required capabilities first.

Provide this Model with Effect.provide (or Layer.provide for subagent handlers). The runtime resolves it on each Thread's first Turn. Supply DecisionModel, native provider clients, and a shared SelectionStore. Direct LanguageModel calls lack thread context and fail with InvalidRequestError; select/restore/resolve remain available for hosts that own selection at admission instead.

Persist record before generation and reuse selection.model for all turns and later runs. Restore rejects a different thread, catalog version, or missing profile; it never silently reselects. Store ownership, authorization, atomic creation, and recovery belong to the host's SelectionStore.

There are no implicit retries, deadlines, fallbacks, confidence thresholds, or mid-thread switches. Defects and interruption propagate. The AutoModel.select span records only the selected profile ID, not task content.

@category ― constructors

@since ― 0.1.0

make
({
version: string
version
: "profiles-v1",
models: {
readonly routine: AutoModel.Candidate<OpenAiClient.OpenAiClient>;
readonly complex: AutoModel.Candidate<OpenAiClient.OpenAiClient>;
}
models
: {
routine: AutoModel.Candidate<OpenAiClient.OpenAiClient>
routine
: {
Candidate<OpenAiClient>.model: Layer.Layer<LanguageModel | ProviderName | ModelName, never, OpenAiClient.OpenAiClient>
model
:
import OpenAiLanguageModel
OpenAiLanguageModel
.
const model: (model: (string & {}) | OpenAiLanguageModel.Model, config?: Omit<{
readonly metadata?: {
readonly [x: string]: string;
} | undefined;
readonly top_logprobs?: number | undefined;
readonly temperature?: number | undefined;
readonly top_p?: number | undefined;
readonly user?: string | undefined;
readonly prompt_cache_key?: string | undefined;
readonly prompt_cache_options?: {
readonly mode?: "explicit" | "implicit" | undefined;
readonly ttl?: "30m" | undefined;
} | undefined;
... 17 more ...;
readonly useItemReferences?: boolean | undefined;
}, "model">) => Model<"openai", LanguageModel, OpenAiClient.OpenAiClient>

Creates an OpenAI model descriptor that can be provided with Effect.provide.

When to use

Use when you want an OpenAI language model value that carries provider and model metadata and can be supplied directly to an Effect program.

@see ― layer for creating a LanguageModel.LanguageModel layer directly

@see ― make for constructing the language model service effectfully

@stability ― unstable

@category ― constructors

@since ― 4.0.0

model
("gpt-6-luna", {
reasoning?: {
readonly effort?: "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh" | undefined;
readonly summary?: "auto" | "concise" | "detailed" | undefined;
readonly generate_summary?: "auto" | "concise" | "detailed" | undefined;
} | undefined
reasoning
: {
effort?: "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh" | undefined
effort
: "medium" } }),
Candidate<Requirements = never>.description: string
description
: "Low cost. Extraction, summaries, and well-specified tasks with clear steps.",
},
complex: AutoModel.Candidate<OpenAiClient.OpenAiClient>
complex
: {
Candidate<OpenAiClient>.model: Layer.Layer<LanguageModel | ProviderName | ModelName, never, OpenAiClient.OpenAiClient>
model
:
import OpenAiLanguageModel
OpenAiLanguageModel
.
const model: (model: (string & {}) | OpenAiLanguageModel.Model, config?: Omit<{
readonly metadata?: {
readonly [x: string]: string;
} | undefined;
readonly top_logprobs?: number | undefined;
readonly temperature?: number | undefined;
readonly top_p?: number | undefined;
readonly user?: string | undefined;
readonly prompt_cache_key?: string | undefined;
readonly prompt_cache_options?: {
readonly mode?: "explicit" | "implicit" | undefined;
readonly ttl?: "30m" | undefined;
} | undefined;
... 17 more ...;
readonly useItemReferences?: boolean | undefined;
}, "model">) => Model<"openai", LanguageModel, OpenAiClient.OpenAiClient>

Creates an OpenAI model descriptor that can be provided with Effect.provide.

When to use

Use when you want an OpenAI language model value that carries provider and model metadata and can be supplied directly to an Effect program.

@see ― layer for creating a LanguageModel.LanguageModel layer directly

@see ― make for constructing the language model service effectfully

@stability ― unstable

@category ― constructors

@since ― 4.0.0

model
("gpt-6-astra", {
reasoning?: {
readonly effort?: "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh" | undefined;
readonly summary?: "auto" | "concise" | "detailed" | undefined;
readonly generate_summary?: "auto" | "concise" | "detailed" | undefined;
} | undefined
reasoning
: {
effort?: "high" | "low" | "max" | "medium" | "minimal" | "none" | "xhigh" | undefined
effort
: "medium" } }),
Candidate<Requirements = never>.description: string
description
: "Higher cost. Difficult reasoning, subtle bugs, and ambiguous requirements.",
},
},
});

Import AutoModel from @yielded/agent-ai-decision. At least two profiles are required. Each profile pairs a native Effect model Layer with a description. Configure reasoning effort and provider options on that Layer; describe capability, cost, and appropriate tasks in the catalog. Supply Jev through TypeSafeDecisionModel, or use another DecisionModel implementation.

const
const program: Effect.Effect<{
readonly output: {
readonly answer: string;
};
readonly finishReason: "completed" | "model-stop" | "budget-exhausted";
readonly turns: number;
readonly threadId: string & Brand<"@effect-agent/core/ThreadId">;
readonly runId: string & Brand<"@effect-agent/core/RunId">;
readonly exhausted?: "tokens" | "tool-calls" | "turns" | undefined;
readonly runDisposition?: Json | undefined;
readonly usage?: RunTotals | undefined;
readonly delegatedUsage?: RunTotals | undefined;
}, AgentRuntime.AgentRuntimeFailure<Definition<String, ... 5 more ..., undefined> & {
...;
}, never, never>, ThreadHistory | ... 3 more ... | SelectionStore>
program
=
import AgentRuntime
AgentRuntime
.
run<Definition<String, Struct<{
readonly answer: String;
}>, "Delegate questions to research when useful, then answer the user.", Toolkit<{
readonly research: Tool<"research", {
readonly parameters: Struct<{
readonly question: String;
}>;
readonly success: Struct<{
readonly output: String;
readonly budgetExhausted: Boolean;
}>;
readonly failure: Subagent.SubagentToolFailure<Never>;
readonly failureMode: "error";
}, AgentRuntime.AgentSpawner | RunEventSink | AgentRuntime.SubagentDurability>;
}>, undefined, undefined, undefined> & {
...;
}, never, never>(agent: Definition<...> & {
...;
}, input: string, options?: RunOptions<...> | undefined): Effect.Effect<...>
export run

Accept schema-encoded input, retaining runtime validation. Use runUnknown for external data.

run
(
const Assistant: Definition<String, Struct<{
readonly answer: String;
}>, "Delegate questions to research when useful, then answer the user.", Toolkit<{
readonly research: Tool<"research", {
readonly parameters: Struct<{
readonly question: String;
}>;
readonly success: Struct<{
readonly output: String;
readonly budgetExhausted: Boolean;
}>;
readonly failure: Subagent.SubagentToolFailure<Never>;
readonly failureMode: "error";
}, AgentRuntime.AgentSpawner | RunEventSink | AgentRuntime.SubagentDurability>;
}>, undefined, undefined, undefined> & {
...;
}
Assistant
, "Compare train and bus travel.").
Pipeable.pipe<Effect.Effect<{
readonly output: {
readonly answer: string;
};
readonly finishReason: "completed" | "model-stop" | "budget-exhausted";
readonly turns: number;
readonly threadId: string & Brand<"@effect-agent/core/ThreadId">;
readonly runId: string & Brand<"@effect-agent/core/RunId">;
readonly exhausted?: "tokens" | "tool-calls" | "turns" | undefined;
readonly runDisposition?: Json | undefined;
readonly usage?: RunTotals | undefined;
readonly delegatedUsage?: RunTotals | undefined;
}, AgentRuntime.AgentRuntimeFailure<Definition<String, ... 5 more ..., undefined> & {
...;
}, never, never>, AgentRuntime.AgentRuntimeRequirements<...>>, Effect.Effect<...>>(this: Effect.Effect<...>, ab: (_: Effect.Effect<...>) => Effect.Effect<...>): Effect.Effect<...> (+21 overloads)
pipe
(
import Effect
Effect
.
const provide: <LanguageModel | ProviderName | ModelName, never, OpenAiClient | DecisionModel | SelectionStore>(layer: Layer.Layer<LanguageModel | ProviderName | ModelName, never, OpenAiClient | DecisionModel | SelectionStore>, options?: {
readonly local?: boolean | undefined;
} | undefined) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<...> (+5 overloads)

Provides dependencies to an effect using layers or a context. Use options.local to build the layer every time; by default, layers are shared between provide calls.

Example (Providing dependencies with a layer)

import { Context, Effect, Layer } from "effect"
interface Database {
readonly query: (sql: string) => Effect.Effect<string>
}
const Database = Context.Service<Database>("Database")
const DatabaseLayer = Layer.succeed(Database)({
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`Result for: ${sql}`))
})
const program = Effect.gen(function*() {
const db = yield* Database
return yield* db.query("SELECT * FROM users")
})
const provided = Effect.provide(program, DatabaseLayer)
await Effect.runPromise(provided) // => "Result for: SELECT * FROM users"

@category ― providing services

@since ― 2.0.0

provide
(
const ThreadModels: AutoModel<OpenAiClient>
ThreadModels
),
);
const
const ResearchLive: Layer.Layer<Handler<"research">, never, OpenAiClient | DecisionModel | SelectionStore | SubagentReservations>
ResearchLive
=
import Subagent
Subagent
.
function layer<"research", Struct<{
readonly question: String;
}>, String, string, {}, Struct<{
readonly question: String;
}>, Struct<{
readonly output: String;
readonly budgetExhausted: Boolean;
}>, Never, never, never, never, "error", never, never, undefined, undefined, undefined>(delegation: Subagent.SubagentDelegation<"research", Struct<{
readonly question: String;
}>, String, string, {}, Struct<{
readonly question: String;
}>, Struct<{
readonly output: String;
readonly budgetExhausted: Boolean;
}>, Never, never, never, "error"> & {
...;
}, modelOrBinding?: undefined, options?: Subagent.SubagentRuntimeOptions<...> | undefined): Layer.Layer<...> (+1 overload)

Build the Toolkit handler Layer using the provided model requirement, or pass a native model / explicit child Binding as an override. Without an override, provide the model with Layer.provide; the handler captures it at construction. AutoModel resolves each new child's own Thread ID and projected first task. Share its selection store across parent Runs and child handler Layers.

Construction requirements carry the child Binding's full runtime needs and both projections; they are captured once via Effect.context so the per-call handler requirements stay exactly the Tool's declared engine dependencies. The handler dispatches on the engine-provided per-batch SubagentDurability service mode:

  • ephemeral (the explicit engine default when no durable coordinator supplied RunOptions.subagent): the S1 path unchanged — preflight, an in-process scoped child Run (SUB-011/012), stable lifecycle events, total-mapped expected child failures, and Scope-finalizer reservation settlement on every exit path.
  • durable (S2): the same fail-closed preflight and input projection, then idempotent establishment through the coordinator (spec §12 steps 2-9) with construction-fixed child Binding digests, encoded grant, and encoded allocation; the engine-owned waiting signal while the attached child is nonterminal; and, on re-entry with a settled child, output decoding, projectResult, and ONE atomic settlement join carrying the conservative accounting summary. Failed children join as the bounded SubagentExecutionFailure; no in-process child fiber ever starts.

layer
(
const Research: Subagent.SubagentDelegation<"research", Struct<{
readonly question: String;
}>, String, string, {}, Struct<{
readonly question: String;
}>, Struct<{
readonly output: String;
readonly budgetExhausted: Boolean;
}>, Never, never, never, "error"> & {
readonly target: Definition<Struct<{
readonly question: String;
}>, String, string, Toolkit<{}>, undefined, undefined, undefined> & {
readonly id: Brand<"@effect-agent/core/AgentId"> & "researcher";
};
}
Research
).
Pipeable.pipe<Layer.Layer<Handler<"research">, never, Subagent.SubagentLayerRequirements<Struct<{
readonly question: String;
}>, String, string, {}, never, never, ModelServices, never, never, never, never, never, undefined, undefined, undefined>>, Layer.Layer<Handler<"research">, never, OpenAiClient | DecisionModel | SelectionStore | SubagentReservations>>(this: Layer.Layer<...>, ab: (_: Layer.Layer<Handler<"research">, never, Subagent.SubagentLayerRequirements<...>>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <OpenAiClient | DecisionModel | SelectionStore, never, LanguageModel | ProviderName | ModelName>(that: Layer.Layer<LanguageModel | ProviderName | ModelName, never, OpenAiClient | DecisionModel | SelectionStore>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, OpenAiClient | DecisionModel | SelectionStore | Exclude<...>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
const ThreadModels: AutoModel<OpenAiClient>
ThreadModels
));

Provide one AutoModel.layerMemory() alongside InMemory.layer, outside the parent program and child handler Layers. The shared store keys choices by thread ID, so siblings use the same catalog without sharing a selection. The selected native model retains its provider identity, client requirements, streaming, tools, and structured output. Building the AutoModel Layer captures dependencies without selecting or acquiring any candidate. It requires an agent thread; for direct LanguageModel calls, explicitly resolve a thread and provide the returned model.

auto-model.ts
export const program = Effect.gen(function* () {
const threadId = Identifiers.ThreadId.make("auto-example");
// AutoModel selects before this thread's first model call.
yield* AgentRuntime.run(Assistant, "Compare taking a train or bus from Lisbon to Porto.", {
threadId,
});
// The same thread keeps its original model on follow-ups.
return yield* AgentRuntime.run(Assistant, "Which would you choose for comfort?", {
threadId,
});
}).pipe(Effect.provide(Live));

The complete example includes Jev, provider clients, and shared Layer assembly.

The runtime supplies the rendered prompt and eligible tool descriptions to the resolver after input validation and inputPrompt projection, before context preparation. Raw input fields excluded by that projection are excluded from selection too. Selection is inside the run’s deadline and interruption scope. Context hooks may prepare prompts but cannot replace the selected model through modelCall; doing so fails with AiError.InvalidRequestError.

AutoModel.SelectionStore.getOrCreate(threadId, select) owns atomic creation and retention. It returns the committed winning SelectionRecord before generation starts. Concurrent resolutions of the same thread share that choice; independent threads can select concurrently. Failed or interrupted selections may retry. A crash before commitment can repeat a selector request. Generative failure after commitment does not change the chosen model.

layerMemory({ capacity? }) retains choices for one Layer lifetime. Capacity defaults to 10,000 distinct attempted thread IDs; entries never expire or evict. New threads fail at capacity while existing choices remain available. Rebuilding the Layer loses selections. Durable hosts must provide a SelectionStore backed by application-owned thread storage, preserving records across restarts. Keep model capacity and pricing configuration aligned with the selected profiles.

Stable model settings avoid implicit changes that can reduce prompt-cache reuse; cache hits still depend on provider behavior and prompt prefixes.

API or field Behavior
make({ models, version, instructions? }) Native Model Layer over approved profiles; construction performs no selection
version Required nonempty version; change when model, effort, or settings change
instructions Optional string classification policy; defaults to the least expensive capable profile for the whole task
resolve({ threadId, state }) Explicit resolution using SelectionStore, DecisionModel, and native client services
layerMemory({ capacity? }) Bounded shared selection storage for ephemeral hosts
SelectionStore Host storage port; durable implementations must atomically retain the winning record
select({ threadId, state }) Explicit one-shot classification for hosts that own admission; re-execution selects again
restore(threadId, record) Validates stored data and returns the native model without decision-provider I/O
SelectionRecord Schema for format version, thread ID, catalog version, profile ID, and decision evidence

Use Schema.encodeEffect(AutoModel.SelectionRecord) at storage boundaries; never serialize model Layers. Retain previous catalogs for active threads. Wrong-thread, malformed, missing-profile, or catalog-version mismatches fail with AiError.InvalidRequestError rather than reselecting. New selections use record version 2: decision.answers.model contains label, probabilities, and optional confidence; decision.usage contains optional token counts. Version 1 records are rejected without mutation or reselection. Hosts with active version 1 threads must retain their previous runtime and catalog until those threads finish, or implement an explicit, data-preserving upgrade in their storage adapter.

The host must restrict candidates to compatible, authorized profiles and send only task context the decision provider may receive. Selection has no implicit retries, fallbacks, or confidence thresholds. Selector usage and confidence live in record.decision, separately from generative run accounting. AutoModel.select tracing records the profile ID without adding task-body logging.

Install @effect/ai-typesafe at the same version as Effect. Its TypeSafeDecisionModel.model(model) provides the native DecisionModel and provider/model identity. It does not provide a LanguageModel.

import {
import TypeSafeClient
TypeSafeClient
,
import TypeSafeDecisionModel
TypeSafeDecisionModel
} from "@effect/ai-typesafe";
import {
import Layer
Layer
} from "effect";
import {
import FetchHttpClient
FetchHttpClient
} from "effect/http";
const
const DecisionLive: Layer.Layer<DecisionModel | ProviderName | ModelName, ConfigError, never>
DecisionLive
=
import TypeSafeDecisionModel
TypeSafeDecisionModel
.
const model: (model: TypeSafeDecisionModel.Model | (string & {})) => Model<"typesafe", DecisionModel, TypeSafeClient.TypeSafeClient>

Creates a decision model with TypeSafe provider metadata.

@stability ― unstable

@category ― constructors

@since ― 4.0.0

model
("jev-latest").
Pipeable.pipe<Model<"typesafe", DecisionModel, TypeSafeClient.TypeSafeClient>, Layer.Layer<DecisionModel | ProviderName | ModelName, ConfigError, HttpClient>, Layer.Layer<DecisionModel | ProviderName | ModelName, ConfigError, never>>(this: Model<...>, ab: (_: Model<"typesafe", DecisionModel, TypeSafeClient.TypeSafeClient>) => Layer.Layer<DecisionModel | ProviderName | ModelName, ConfigError, HttpClient>, bc: (_: Layer.Layer<...>) => Layer.Layer<...>): Layer.Layer<...> (+21 overloads)
pipe
(
import Layer
Layer
.
const provide: <HttpClient, ConfigError, TypeSafeClient.TypeSafeClient>(that: Layer.Layer<TypeSafeClient.TypeSafeClient, ConfigError, HttpClient>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, ConfigError | E2, HttpClient | Exclude<RIn2, TypeSafeClient.TypeSafeClient>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
import TypeSafeClient
TypeSafeClient
.
const layerConfig: (options?: {
readonly apiKey?: Config<Redacted<string> | undefined> | undefined;
readonly apiUrl?: Config<string> | undefined;
readonly transformClient?: ((client: HttpClient) => HttpClient) | undefined;
}) => Layer.Layer<TypeSafeClient.TypeSafeClient, ConfigError, HttpClient>

Provides a client from configuration, defaulting to TYPESAFE_API_KEY.

@stability ― unstable

@category ― layers

@since ― 4.0.0

layerConfig
()),
import Layer
Layer
.
const provide: <never, never, HttpClient>(that: Layer.Layer<HttpClient, never, never>) => <RIn2, E2, ROut2>(self: Layer.Layer<ROut2, E2, RIn2>) => Layer.Layer<ROut2, E2, Exclude<RIn2, HttpClient>> (+3 overloads)

Feeds the output services of the dependency layer into the requirements of this layer, returning a layer that only provides the services from this layer.

When to use

Use when you need to hide an implementation dependency layer from callers.

Details

In serviceLayer.pipe(Layer.provide(dependencyLayer)), the dependency layer is built first and is used to satisfy the requirements of serviceLayer.

Example (Providing layer dependencies)

import { Context, Effect, Layer } from "effect"
class Database extends Context.Service<Database, {
readonly query: (sql: string) => Effect.Effect<string>
}>()("Database") {}
class UserService extends Context.Service<UserService, {
readonly getUser: (id: string) => Effect.Effect<{
id: string
name: string
}>
}>()("UserService") {}
class Logger extends Context.Service<Logger, {
readonly log: (msg: string) => Effect.Effect<void>
}>()("Logger") {}
// Create dependency layers
const databaseLayer = Layer.succeed(Database, {
query: Effect.fn("Database.query")((sql: string) => Effect.succeed(`DB: ${sql}`))
})
const logs: Array<string> = []
const loggerLayer = Layer.succeed(Logger, {
log: Effect.fn("Logger.log")((msg: string) => Effect.sync(() => logs.push(`[LOG] ${msg}`)))
})
// UserService depends on Database and Logger
const userServiceLayer = Layer.effect(UserService, Effect.gen(function*() {
const database = yield* Database
const logger = yield* Logger
return {
getUser: Effect.fn("UserService.getUser")(function*(id: string) {
yield* logger.log(`Looking up user ${id}`)
const result = yield* database.query(
`SELECT * FROM users WHERE id = ${id}`
)
return { id, name: result }
})
}
}))
// Provide dependencies to UserService layer
const userServiceWithDependencies = userServiceLayer.pipe(
Layer.provide(Layer.mergeAll(databaseLayer, loggerLayer))
)
// Now UserService layer has no dependencies
const program = Effect.gen(function*() {
const userService = yield* UserService
return yield* userService.getUser("123")
}).pipe(
Effect.provide(userServiceWithDependencies)
)
Effect.runSync(program) // => { id: "123", name: "DB: SELECT * FROM users WHERE id = 123" }
logs // => ["[LOG] Looking up user 123"]

@see ― provideMerge for retaining the dependency services

@category ― providing services

@since ― 2.0.0

provide
(
import FetchHttpClient
FetchHttpClient
.
const layer: Layer.Layer<HttpClient, never, never>

Layer that provides an HttpClient implementation backed by the configured Fetch function.

When to use

Use when an Effect program should execute HttpClient requests through the platform fetch implementation, especially in browser, edge, or Node.js runtimes with globalThis.fetch.

Details

The layer uses the current Fetch reference and optional RequestInit service for each request. Request-specific method, headers, body, and abort signal are supplied by the client and override matching RequestInit fields.

Gotchas

Fetch behavior comes from the runtime's implementation, so CORS, cookies, redirects, abort handling, and streaming support can vary by platform. Stream request bodies are sent as Web streams with duplex: "half", and any content-length header is removed before calling fetch.

@see ― Fetch for supplying the fetch implementation used by this layer

@see ― RequestInit for default RequestInit options applied before request-specific fields

@stability ― unstable

@category ― layers

@since ― 4.0.0

layer
),
);

TypeSafeClient.layerConfig() reads TYPESAFE_API_KEY. To read a custom base URL, pass { apiUrl: Config.string("TYPESAFE_API_URL") }; the default is https://api.typesafe.ai/v1. Use TypeSafeClient.layer({ apiKey, apiUrl, transformClient }) for explicit configuration. The API key is a Redacted<string>. Construction performs no requests.

For low-level access, obtain TypeSafeClient.TypeSafeClient and call client.systemOne({ model, state, questions }) or client.listModels(). TypeSafeSchema exposes their wire schemas. System One uses choice, score, and noul questions. Use DecisionModel.decide for request-derived answer types and validated probability distributions; systemOne returns the provider’s wire answer union. The direct example shows bounded retries and a timeout.

Native decision input encoding failures use AiError.InvalidUserInputError; invalid answers use AiError.InvalidOutputError. TypeSafe maps HTTP failures to typed AiError reasons, including authentication, rate limiting, and provider failures. Configuration can fail with ConfigError. Defects and interruption propagate.

There are no automatic retries or deadlines. Compose Effect.retry and Effect.timeout, or configure HTTP policies through the client’s transformClient option or TypeSafeConfig.withClientTransform. Rate-limit errors retain retry delays when available. HTTP error text may include submitted content; the host controls tracing and logging.

Former API Native API
DecisionSet.make({ input, questions }) Decision.make({ input, decisions })
DecisionQuery.choice({ options }) Decision.classify({ criteria })
DecisionQuery.score({ levels }) Decision.rate({ criteria })
DecisionQuery.probability Decision.probability, with optional outcome descriptions
model.evaluate(set, input) DecisionModel.decide(definition, { input })
Choice .choice / score .score Classification .label / rating .rating
@effect-agent/ai-typesafe @effect/ai-typesafe
TypeSafeClient.Config.layer + TypeSafeClient.layer TypeSafeClient.layerConfig()
client.evaluate(request) client.systemOne(request)

@yielded/agent-ai-decision exports AutoModel and LanguageModelDecisionModel. Import the shared decision APIs directly from Effect; provider integrations come directly from upstream. Custom providers implement DecisionModel.make({ decide }), returning tagged provider answers and usage.