Skip to content

Prompts and Responses

Every call to a LanguageModel flows through two provider-neutral data models:

  • Prompt — the conversation you send into the model: an ordered list of messages (system, user, assistant, tool), each made up of typed content parts (text, files, tool calls, tool results, reasoning, approval).
  • Response — the model output that comes back: an ordered list of typed response parts (text, reasoning, tool calls, tool results, sources, metadata, finish info, errors), with both a non-streaming and a streaming taxonomy.

Both live in the effect/unstable/ai package:

import { Prompt, Response } from "effect/unstable/ai"

The prompt option of generateText / streamText is typed as Prompt.RawInput, which accepts three shapes: a string, an array of encoded messages, or an already-built Prompt.

import { Prompt } from "effect/unstable/ai"
// 1. A string — becomes a single user message
Prompt.make("Summarize the Effect docs in one sentence.")
// 2. An array of (encoded) messages
Prompt.make([
{ role: "system", content: "You are a terse assistant." },
{ role: "user", content: [{ type: "text", text: "Say hi." }] }
])
// 3. An existing Prompt is passed through unchanged
const existing = Prompt.make("hello")
Prompt.make(existing) // => existing

In a LanguageModel call you usually pass the RawInput directly:

import { Effect } from "effect"
import { LanguageModel } from "effect/unstable/ai"
const program = Effect.gen(function* () {
// string RawInput
const a = yield* LanguageModel.generateText({
prompt: "What is 2 + 2?"
})
// message-array RawInput
const b = yield* LanguageModel.generateText({
prompt: [
{ role: "system", content: "You only answer with numbers." },
{ role: "user", content: [{ type: "text", text: "What is 2 + 2?" }] }
]
})
return [a.text, b.text]
})

Use the role-specific constructors when you want typed, explicit messages — for example when storing and replaying conversation history.

import { Prompt } from "effect/unstable/ai"
const prompt = Prompt.fromMessages([
Prompt.systemMessage({
content: "You are a helpful assistant specialized in mathematics."
}),
Prompt.userMessage({
content: [Prompt.textPart({ text: "What is the derivative of x²?" })]
}),
Prompt.assistantMessage({
content: [Prompt.textPart({ text: "The derivative of x² is 2x." })]
})
])
prompt.content.length // => 3

setSystem, prependSystem, appendSystem, and concat let you layer system instructions and merge prompts without mutating the originals:

import { Prompt } from "effect/unstable/ai"
const base = Prompt.make("Write a haiku about TypeScript.")
const withSystem = base.pipe(
Prompt.prependSystem("You are a poet. ")
)
// => system: "You are a poet. " + user: "Write a haiku about TypeScript."
const longer = Prompt.concat(withSystem, "Now translate it to French.")
// => appends a second user message

LanguageModel.generateText resolves to a GenerateTextResponse whose accessors pull the relevant Response parts out of response.content for you.

import { Effect } from "effect"
import { LanguageModel } from "effect/unstable/ai"
const program = Effect.gen(function* () {
const response = yield* LanguageModel.generateText({
prompt: "What's the weather in SF?"
})
response.text // => the concatenated text of all TextParts
response.finishReason // => "stop" | "length" | "tool-calls" | ...
response.usage // => Response.Usage instance
response.toolCalls // => ReadonlyArray<Response.ToolCallPart<...>>
response.toolResults // => ReadonlyArray<Response.ToolResultPart<...>>
response.content // => the raw ordered Array<Response.Part>
return response.text
})

If you need finer control, iterate response.content and switch on part.type — every part carries a type discriminator:

import { Effect } from "effect"
import { LanguageModel } from "effect/unstable/ai"
const program = Effect.gen(function* () {
const response = yield* LanguageModel.generateText({
prompt: "Call a tool, then summarize."
})
for (const part of response.content) {
switch (part.type) {
case "text":
console.log("text:", part.text)
break
case "tool-call":
console.log("calling", part.name, "with", part.params)
break
case "tool-result":
console.log("result for", part.name, "=>", part.result)
break
case "finish":
console.log("done because", part.reason)
break
}
}
})

LanguageModel.streamText produces a Stream of Response.StreamParts. Text arrives as a text-start / text-delta / text-end sequence keyed by a shared id; reasoning and tool parameters follow the same start/delta/end pattern. Accumulate the deltas, then treat the completed text as final.

import { Effect, Stream } from "effect"
import { LanguageModel } from "effect/unstable/ai"
const program = Effect.gen(function* () {
yield* LanguageModel.streamText({
prompt: "Stream a short story."
}).pipe(
Stream.runForEach((part) =>
Effect.sync(() => {
switch (part.type) {
case "text-delta":
process.stdout.write(part.delta) // incremental chunk
break
case "tool-call":
console.log("\ntool-call:", part.name)
break
case "finish":
console.log("\nfinished:", part.reason)
break
}
})
)
)
})

A Prompt is { content: ReadonlyArray<Message> } plus a brand. Build prompts with the constructors below rather than constructing the object literal.

The union accepted by make and concat: string | Iterable<MessageEncoded> | Prompt. A string becomes a single user message; an iterable is decoded as encoded messages.

import type { Prompt } from "effect/unstable/ai"
const a: Prompt.RawInput = "hi"
const b: Prompt.RawInput = [{ role: "user", content: "hi" }]

Prompt.Prompt is the model type; the Prompt.Prompt const is a Schema.Codec<Prompt, PromptEncoded> for decoding/encoding whole conversations (e.g. to persist them).

import { Schema } from "effect"
import { Prompt } from "effect/unstable/ai"
const decode = Schema.decodeUnknownSync(Prompt.Prompt)
const prompt = decode({
content: [{ role: "user", content: "Hello" }]
})
prompt.content.length // => 1

The serialized shape: { content: ReadonlyArray<MessageEncoded> }. This is what Schema.encode(Prompt.Prompt) produces and what you store on disk / in a DB.

A Prompt with no messages — a useful identity for folds and concat.

import { Prompt } from "effect/unstable/ai"
Prompt.empty.content // => []

The primary constructor. Normalizes any RawInput into a Prompt.

import { Prompt } from "effect/unstable/ai"
Prompt.make("Hello").content.length // => 1 (a user message)

Builds a Prompt directly from an array of already-constructed Messages (no decoding step).

import { Prompt } from "effect/unstable/ai"
const prompt = Prompt.fromMessages([
Prompt.systemMessage({ content: "Be brief." }),
Prompt.userMessage({ content: [Prompt.textPart({ text: "Hi" })] })
])
prompt.content.length // => 2

Folds an array of Response.AnyParts back into a Prompt — completed text and reasoning streams become assistant parts, tool calls/approvals go in an assistant message, and non-preliminary tool results go in a tool message. This is how you append a model turn to conversation history.

import { Prompt, Response } from "effect/unstable/ai"
const prompt = Prompt.fromResponseParts([
Response.makePart("text", { text: "Hello there!" })
])
prompt.content[0].role // => "assistant"

Appends one RawInput’s messages after another prompt’s messages, preserving order. Dual-signature (data-first and data-last/pipeable).

import { Prompt } from "effect/unstable/ai"
const sys = Prompt.make([{ role: "system", content: "You are helpful." }])
Prompt.concat(sys, "Hello!").content.length // => 2

Returns a new prompt whose system message is replaced by the given text, dropping any previous system messages.

import { Prompt } from "effect/unstable/ai"
const p = Prompt.make([{ role: "system", content: "Old." }, { role: "user", content: "Hi" }])
Prompt.setSystem(p, "New.").content[0] // => system message with content "New."

Prepends content to the existing system message (or creates one), leaving a leading system message.

import { Prompt } from "effect/unstable/ai"
const p = Prompt.make([{ role: "system", content: "an expert." }])
Prompt.prependSystem(p, "You are ").content[0]
// => system content: "You are an expert."

Like prependSystem, but appends content to the existing system message instead.

import { Prompt } from "effect/unstable/ai"
const p = Prompt.make([{ role: "system", content: "You are an expert." }])
Prompt.appendSystem(p, " Be concise.").content[0]
// => system content: "You are an expert. Be concise."

Type guard for a Prompt value.

import { Prompt } from "effect/unstable/ai"
Prompt.isPrompt(Prompt.empty) // => true
Prompt.isPrompt("hello") // => false

Type guards for a prompt Message and a prompt content Part respectively.

import { Prompt } from "effect/unstable/ai"
Prompt.isMessage(Prompt.systemMessage({ content: "hi" })) // => true
Prompt.isPart(Prompt.textPart({ text: "hi" })) // => true

Generic message constructor — pass a role literal and the role’s params. The role-specific helpers below wrap this.

import { Prompt } from "effect/unstable/ai"
Prompt.makeMessage("user", {
content: [Prompt.makePart("text", { text: "Hi" })]
}).role // => "user"

Generic part constructor — pass a part type literal and its params. The part-specific helpers below wrap this.

import { Prompt } from "effect/unstable/ai"
Prompt.makePart("text", { text: "Hi" }).type // => "text"

Prompt.Message is the union SystemMessage | UserMessage | AssistantMessage | ToolMessage. Each role has an interface, a Schema const of the same name, and a lowercase constructor.

System instructions/context. content is plain text.

import { Prompt } from "effect/unstable/ai"
Prompt.systemMessage({ content: "You are a helpful assistant." }).role
// => "system"

User input. content is an array of TextPart | FilePart (the schema also decodes a bare string into a single text part via ContentFromString).

import { Prompt } from "effect/unstable/ai"
Prompt.userMessage({
content: [
Prompt.textPart({ text: "What is in this image?" }),
Prompt.filePart({ mediaType: "image/jpeg", data: "data:image/jpeg;base64,..." })
]
}).role // => "user"

Model responses for history. content may contain text, file, reasoning, tool-call, tool-result, and tool-approval-request parts.

import { Prompt } from "effect/unstable/ai"
Prompt.assistantMessage({
content: [
Prompt.textPart({ text: "Let me check the weather." }),
Prompt.toolCallPart({
id: "call_1",
name: "get_weather",
params: { city: "SF" },
providerExecuted: false
})
]
}).role // => "assistant"

Tool-side content: ToolResultParts and ToolApprovalResponseParts answering an assistant’s requests.

import { Prompt } from "effect/unstable/ai"
Prompt.toolMessage({
content: [
Prompt.toolResultPart({
id: "call_1",
name: "get_weather",
isFailure: false,
result: { temperature: 72 }
})
]
}).role // => "tool"

Prompt.Message is also a Schema.Codec over the message union, used internally by the Prompt codec. Prompt.ContentFromString is the transform that lets a string stand in for [{ type: "text", text }] when decoding user/assistant content.

import { Schema } from "effect"
import { Prompt } from "effect/unstable/ai"
const m = Schema.decodeUnknownSync(Prompt.Message)({
role: "user",
content: "Hello" // decoded into a single TextPart
})
m.role // => "user"

Prompt.Part is the union of all content parts. Each has an interface, a Schema const of the same name, and a lowercase constructor. All parts carry an options field of type ProviderOptions (defaults to {}).

Plain text content. The most common part.

import { Prompt } from "effect/unstable/ai"
Prompt.textPart({ text: "Hello, world!" }).type // => "text"

Provider-supplied reasoning summary attached to an assistant message. Not raw hidden chain-of-thought.

import { Prompt } from "effect/unstable/ai"
Prompt.reasoningPart({ text: "Compared options by price." }).type
// => "reasoning"

A file attachment. data may be a base64/data-URL string, a Uint8Array, or a URL.

import { Prompt } from "effect/unstable/ai"
Prompt.filePart({
mediaType: "application/pdf",
fileName: "report.pdf",
data: new Uint8Array([1, 2, 3])
}).mediaType // => "application/pdf"

A request to invoke a tool. providerExecuted distinguishes provider-run from framework-run tools.

import { Prompt } from "effect/unstable/ai"
Prompt.toolCallPart({
id: "call_123",
name: "get_weather",
params: { city: "San Francisco" },
providerExecuted: false
}).name // => "get_weather"

The result of a tool call to feed back into the conversation. isFailure flags whether the handler errored.

import { Prompt } from "effect/unstable/ai"
Prompt.toolResultPart({
id: "call_123",
name: "get_weather",
isFailure: false,
result: { temperature: 22, condition: "sunny" }
}).isFailure // => false

ToolApprovalRequestPart / toolApprovalRequestPart

Section titled “ToolApprovalRequestPart / toolApprovalRequestPart”

Stored in an assistant message when a tool needs user approval before running. Pairs an approvalId with the toolCallId awaiting approval.

import { Prompt } from "effect/unstable/ai"
Prompt.toolApprovalRequestPart({
approvalId: "approval_123",
toolCallId: "call_456"
}).approvalId // => "approval_123"

ToolApprovalResponsePart / toolApprovalResponsePart

Section titled “ToolApprovalResponsePart / toolApprovalResponsePart”

The user’s answer to an approval request, placed in a tool message. Carries approved and an optional reason.

import { Prompt } from "effect/unstable/ai"
Prompt.toolApprovalResponsePart({
approvalId: "approval_123",
approved: false,
reason: "Operation not allowed"
}).approved // => false

Prompt.ProviderOptions is a Schema.Record<String, Json | null> for provider-specific options attached to any message or part via its options field. Augment per-part *Options interfaces to type these for a provider.

import type { Prompt } from "effect/unstable/ai"
const opts: Prompt.ProviderOptions = {
anthropic: { cacheControl: { type: "ephemeral" } }
}

A response is an ordered sequence of typed parts. Non-streaming responses use complete parts (TextPart, ToolCallPart, …); streaming responses use event parts (TextStartPart, TextDeltaPart, …). Every response part carries a metadata field of type ProviderMetadata (defaults to {}) — note this is metadata, distinct from a prompt part’s options.

Generic response-part constructor: a type literal plus its params. Tool-aware parts have dedicated constructors below.

import { Response } from "effect/unstable/ai"
Response.makePart("text", { text: "Hello" }).type // => "text"

Type guard for any response part.

import { Response } from "effect/unstable/ai"
Response.isPart(Response.makePart("text", { text: "hi" })) // => true

These appear in generateText output (response.content).

Plain text content. response.text concatenates the text of all TextParts.

import { Response } from "effect/unstable/ai"
Response.makePart("text", { text: "The answer is 42." }).text
// => "The answer is 42."

Provider-exposed reasoning summary. Read via response.reasoningText.

import { Response } from "effect/unstable/ai"
Response.makePart("reasoning", { text: "Step 1: analyze the question..." }).type
// => "reasoning"

A tool invocation requested by the model. Generic over Name and Params so the parameters stay typed to the tool. Response.ToolCallPart(name, paramsSchema) builds a schema for a specific tool.

import { Response } from "effect/unstable/ai"
const part = Response.toolCallPart({
id: "call_123",
name: "get_weather",
params: { city: "San Francisco" },
providerExecuted: false
})
part.params // => { city: "San Francisco" }

The decoded result of a tool call. It is a union of ToolResultSuccess and ToolResultFailure (discriminated by isFailure), both carrying the decoded result, the encodedResult, providerExecuted, and preliminary flags. Response.ToolResultPart(name, success, failure) builds a tool-specific schema.

import { Response } from "effect/unstable/ai"
// success variant
const ok = Response.toolResultPart({
id: "call_123",
name: "get_weather",
isFailure: false,
result: { temperature: 22 },
encodedResult: { temperature: 22 },
providerExecuted: false,
preliminary: false
})
ok.isFailure // => false
// failure variant
const bad = Response.toolResultPart({
id: "call_124",
name: "get_weather",
isFailure: true,
result: { message: "city not found" },
encodedResult: { message: "city not found" },
providerExecuted: false,
preliminary: false
})
bad.isFailure // => true

ToolApprovalRequestPart / toolApprovalRequestPart

Section titled “ToolApprovalRequestPart / toolApprovalRequestPart”

Emitted instead of running a tool when the tool’s needsApproval is set. Pairs an approvalId with the toolCallId.

import { Response } from "effect/unstable/ai"
Response.toolApprovalRequestPart({
approvalId: "approval_123",
toolCallId: "call_456"
}).toolCallId // => "call_456"

A binary file returned by the model. data is a Uint8Array when decoded (a base64 string when encoded).

import { Response } from "effect/unstable/ai"
Response.makePart("file", {
mediaType: "image/png",
data: new Uint8Array([1, 2, 3])
}).mediaType // => "image/png"

Citations for sources used to generate the response. Both have type: "source" discriminated by sourceType ("document" vs "url").

import { Response } from "effect/unstable/ai"
const url = Response.makePart("source", {
sourceType: "url",
id: "src_1",
url: new URL("https://effect.website"),
title: "Effect"
})
url.sourceType // => "url"
const doc = Response.makePart("source", {
sourceType: "document",
id: "src_2",
mediaType: "application/pdf",
title: "Spec"
})
doc.sourceType // => "document"

Per-response metadata: provider id, modelId, timestamp, and the HTTP request details. All fields may be undefined.

import { DateTime } from "effect"
import { Response } from "effect/unstable/ai"
Response.makePart("response-metadata", {
id: "resp_123",
modelId: "gpt-4",
timestamp: DateTime.nowUnsafe(),
request: undefined
}).modelId // => "gpt-4"

The terminal part of every response: the reason, token usage, and optional HTTP response details. response.finishReason and response.usage read from this part.

import { Response } from "effect/unstable/ai"
Response.makePart("finish", {
reason: "stop",
usage: new Response.Usage({
inputTokens: { uncached: undefined, total: 50, cacheRead: undefined, cacheWrite: undefined },
outputTokens: { total: 25, text: undefined, reasoning: undefined }
}),
response: undefined
}).reason // => "stop"

Signals an error occurred while generating. error is unknown — narrow it before reading Error fields.

import { Response } from "effect/unstable/ai"
Response.makePart("error", { error: new Error("boom") }).type // => "error"

In addition to the result parts above (which can also appear mid-stream), a streamed response emits start/delta/end events. Accumulate deltas keyed by id.

TextStartPart / TextDeltaPart / TextEndPart

Section titled “TextStartPart / TextDeltaPart / TextEndPart”

A text chunk lifecycle: text-start opens an id, each text-delta adds a delta fragment, text-end closes it.

import { Response } from "effect/unstable/ai"
Response.makePart("text-start", { id: "t1" }).type // => "text-start"
Response.makePart("text-delta", { id: "t1", delta: "Hel" }).delta // => "Hel"
Response.makePart("text-end", { id: "t1" }).type // => "text-end"

ReasoningStartPart / ReasoningDeltaPart / ReasoningEndPart

Section titled “ReasoningStartPart / ReasoningDeltaPart / ReasoningEndPart”

The same start/delta/end lifecycle for streamed reasoning text.

import { Response } from "effect/unstable/ai"
Response.makePart("reasoning-start", { id: "r1" }).type // => "reasoning-start"
Response.makePart("reasoning-delta", { id: "r1", delta: "Think" }).delta // => "Think"
Response.makePart("reasoning-end", { id: "r1" }).type // => "reasoning-end"

ToolParamsStartPart / ToolParamsDeltaPart / ToolParamsEndPart

Section titled “ToolParamsStartPart / ToolParamsDeltaPart / ToolParamsEndPart”

Streamed tool-call parameters. tool-params-start includes the tool name and providerExecuted; deltas are JSON fragments; tool-params-end closes the chunk, after which a complete tool-call part follows.

import { Response } from "effect/unstable/ai"
Response.makePart("tool-params-start", {
id: "tp1",
name: "get_weather",
providerExecuted: false
}).name // => "get_weather"
Response.makePart("tool-params-delta", { id: "tp1", delta: '{"city":' }).delta
// => '{"city":'
Response.makePart("tool-params-end", { id: "tp1" }).type // => "tool-params-end"

The Response module distinguishes three families. The type-level unions describe the shapes; the like-named functions build a tool-aware Schema.Codec from a Toolkit so tool-call params and tool-result payloads stay aligned with each tool definition.

  • AnyPart / AnyPartEncoded — every possible response part (tool parts use any).
  • Part<Tools> / PartEncoded — non-streaming result parts, tool-typed.
  • StreamPart<Tools> / StreamPartEncoded — streaming event parts, tool-typed.
  • AllParts<Tools> / AllPartsEncoded — the full set (result + stream), tool-typed.
import type { Response } from "effect/unstable/ai"
// StreamPart is what Stream.runForEach receives from streamText
declare const part: Response.StreamPart<{}>
part.type // => one of the streaming part type literals

AllParts / Part / StreamPart (schema builders)

Section titled “AllParts / Part / StreamPart (schema builders)”

Each takes a Toolkit and returns a Schema.Codec over the corresponding union, with ToolCallPart/ToolResultPart schemas generated per tool.

import { Schema } from "effect"
import { Response, Tool, Toolkit } from "effect/unstable/ai"
const toolkit = Toolkit.make(
Tool.make("GetWeather", {
parameters: Schema.Struct({ city: Schema.String }),
success: Schema.Struct({ temperature: Schema.Number })
})
)
const partSchema = Response.Part(toolkit) // non-streaming, tool-aware
const streamSchema = Response.StreamPart(toolkit) // streaming, tool-aware
const allSchema = Response.AllParts(toolkit) // both

A Schema.Class of token-usage statistics, split into inputTokens (uncached, total, cacheRead, cacheWrite) and outputTokens (total, text, reasoning). Every field is number | undefined.

import { Response } from "effect/unstable/ai"
const usage = new Response.Usage({
inputTokens: { uncached: 40, total: 50, cacheRead: 10, cacheWrite: 0 },
outputTokens: { total: 25, text: 20, reasoning: 5 }
})
usage.outputTokens.total // => 25

A Schema.Literals of why generation stopped: "stop", "length", "content-filter", "tool-calls", "error", "pause", "other", "unknown".

import type { Response } from "effect/unstable/ai"
const reason: Response.FinishReason = "tool-calls"

Schema.Record<String, Json | null> for provider-specific metadata attached to any response part via its metadata field. Provider extra usage info typically lands here on the FinishPart.

import type { Response } from "effect/unstable/ai"
const meta: Response.ProviderMetadata = {
openai: { systemFingerprint: "fp_123" }
}

Schemas describing the HTTP exchange with the provider. HttpRequestDetails (on ResponseMetadataPart.request) captures method, url, urlParams, hash, and headers; HttpResponseDetails (on FinishPart.response) captures status and headers. Header values may be Redacted.

import type { Response } from "effect/unstable/ai"
const details: typeof Response.HttpResponseDetails.Type = {
status: 200,
headers: { "X-Request-Id": "req_abc123" }
}

Given a response part type, the params object accepted by its constructor — the part minus the brand, type, sourceType, and metadata (which is optional).

import type { Response } from "effect/unstable/ai"
type Params = Response.ConstructorParams<Response.TextPart>
// => { readonly text: string; readonly metadata?: ... }

Type-level helpers that map a Record<string, Tool.Any> to the union of its ToolCallParts / ToolResultParts. Used internally to build Part, StreamPart, and AllParts.

import type { Response, Tool } from "effect/unstable/ai"
declare const tools: Record<string, Tool.Any>
type Calls = Response.ToolCallParts<typeof tools>
type Results = Response.ToolResultParts<typeof tools>
  • LanguageModel — produces a Prompt from RawInput and returns responses with .text, .toolCalls, .usage, and friends.
  • Tools and Toolkits — define the tools whose schemas drive the tool-aware Response.Part / StreamPart / AllParts builders.