| name | effect-ai-prompt |
| description | Build prompts for Effect AI using messages, parts, and composition operators. Covers the complete Prompt API for constructing, merging, and manipulating conversations with language models. |
Effect AI Prompt Construction
Master the Effect AI Prompt API for building type-safe conversations with language models.
Import Patterns
CRITICAL: Always use namespace imports:
import * as Prompt from 'effect/unstable/ai/Prompt';
import * as Response from 'effect/unstable/ai/Response';
import { pipe } from 'effect';
When to Use This Skill
- Constructing messages for language model requests
- Building multi-turn conversation history
- Adding system instructions to prompts
- Integrating tool calls and results into conversations
- Converting streaming responses to prompt history
- Managing file/image attachments in messages
- Implementing custom chat interfaces
Conceptual Model
-- Message hierarchy
type Message = SystemMessage | UserMessage | AssistantMessage | ToolMessage
type Part =
| TextPart
| ReasoningPart
| FilePart
| ToolCallPart
| ToolResultPart
| ToolApprovalRequestPart
| ToolApprovalResponsePart
-- Composition
Prompt.make :: RawInput → Prompt
Prompt.concat :: (Prompt, RawInput) → Prompt
Prompt.setSystem :: (Prompt, String) → Prompt
-- History transformation
fromResponseParts :: ReadonlyArray<Response.Part> → Prompt
Message Types
Each message has role and content. Content is an array of Part objects.
System Messages
import * as Prompt from 'effect/unstable/ai/Prompt';
const system = Prompt.makeMessage('system', {
content: 'You are a helpful assistant specialized in mathematics.'
});
const systemShorthand = Prompt.systemMessage({
content: 'You are a helpful assistant specialized in mathematics.'
});
const systemWithOptions = Prompt.makeMessage('system', {
content: 'You are an expert coder.',
options: {
anthropic: { cache_control: { type: 'ephemeral' } }
}
});
User Messages
const userText = Prompt.makeMessage("user", {
content: [
Prompt.makePart("text", { text: "What is 2+2?" })
]
})
const userShorthand = Prompt.userMessage({
content: [
Prompt.makePart("text", { text: "What is 2+2?" })
]
})
const userMultimodal = Prompt.makeMessage("user", {
content: [
Prompt.makePart("text", { text: "What's in this image?" }),
Prompt.makePart("file", {
mediaType: "image/jpeg",
fileName: "photo.jpg",
data: new Uint8Array([...])
})
]
})
Assistant Messages
const assistant = Prompt.makeMessage('assistant', {
content: [
Prompt.makePart('reasoning', {
text: 'I need to check the weather using the tool.'
}),
Prompt.makePart('tool-call', {
id: 'call_123',
name: 'get_weather',
params: { city: 'Paris' },
providerExecuted: false
}),
Prompt.makePart('tool-approval-request', {
approvalId: 'approval_123',
toolCallId: 'call_123'
}),
Prompt.makePart('text', {
text: 'The weather in Paris is sunny, 22°C.'
})
]
});
const assistantShorthand = Prompt.assistantMessage({
content: [
Prompt.makePart('text', {
text: 'The weather in Paris is sunny, 22°C.'
})
]
});
Tool Messages
const toolMessage = Prompt.makeMessage('tool', {
content: [
Prompt.makePart('tool-result', {
id: 'call_123',
name: 'get_weather',
isFailure: false,
result: { temperature: 22, condition: 'sunny' }
})
]
});
const toolShorthand = Prompt.toolMessage({
content: [
Prompt.makePart('tool-result', {
id: 'call_123',
name: 'get_weather',
isFailure: false,
result: { temperature: 22, condition: 'sunny' }
})
]
});
Part Types
Text Part
const textPart = Prompt.makePart('text', {
text: 'Hello, world!'
});
Reasoning Part
const reasoningPart = Prompt.makePart('reasoning', {
text: 'Let me think step by step...'
});
File Part
const fileFromUrl = Prompt.makePart('file', {
mediaType: 'image/png',
fileName: 'screenshot.png',
data: new URL('https://example.com/image.png')
});
const fileFromBytes = Prompt.makePart('file', {
mediaType: 'application/pdf',
fileName: 'report.pdf',
data: new Uint8Array([1, 2, 3])
});
const fileFromBase64 = Prompt.makePart('file', {
mediaType: 'image/jpeg',
data: 'data:image/jpeg;base64,/9j/4AAQ...'
});
For OpenAI file parts, strings are provider file IDs only when OpenAiLanguageModel.Config.fileIdPrefixes matches the string prefix (for example file-). Use URL or Uint8Array for inline content; the OpenAI adapter encodes bytes as base64 data.
Tool Call Part
const toolCall = Prompt.makePart('tool-call', {
id: 'call_abc123',
name: 'calculate',
params: { expression: '2 + 2' },
providerExecuted: false
});
Tool Result Part
const toolResult = Prompt.makePart('tool-result', {
id: 'call_abc123',
name: 'calculate',
isFailure: false,
result: 4
});
Tool Approval Parts
const approvalRequest = Prompt.makePart('tool-approval-request', {
approvalId: 'approval_abc123',
toolCallId: 'call_abc123'
});
const approvalResponse = Prompt.toolApprovalResponsePart({
approvalId: 'approval_abc123',
approved: true
});
const denialResponse = Prompt.toolApprovalResponsePart({
approvalId: 'approval_def456',
approved: false,
reason: 'Operation not allowed'
});
Tool approval requests live in assistant messages. Approval responses live in tool messages and are collected on the next model call.
Prompt Construction
From String
const prompt = Prompt.make('Hello, how are you?');
From Encoded Messages
const prompt = Prompt.make([
{ role: 'system', content: 'You are helpful.' },
{ role: 'user', content: [{ type: 'text', text: 'Hi!' }] }
]);
Prompt.RawInput is exactly string | Iterable<Prompt.MessageEncoded> | Prompt.Prompt: strings become one user text message, iterables are decoded encoded messages, and existing prompts pass through unchanged.
From Existing Prompt
const copy = Prompt.make(existingPrompt);
Empty Prompt
const empty = Prompt.empty;
From Messages Array
const messages: ReadonlyArray<Prompt.Message> = [
Prompt.systemMessage({ content: 'You are an expert.' }),
Prompt.userMessage({
content: [Prompt.makePart('text', { text: 'Help me.' })]
})
];
const prompt = Prompt.fromMessages(messages);
const prompt2 = Prompt.make([
Prompt.makeMessage('system', { content: 'You are an expert.' }),
Prompt.makeMessage('user', {
content: [Prompt.makePart('text', { text: 'Help me.' })]
})
]);
Prompt Composition
Merge Prompts
import { pipe } from 'effect';
const systemPrompt = Prompt.make([
{
role: 'system',
content: 'You are a coding assistant.'
}
]);
const userPrompt = Prompt.make('Write a function');
const combined = pipe(systemPrompt, Prompt.concat(userPrompt));
const combined2 = Prompt.concat(systemPrompt, userPrompt);
System Message Manipulation
import { pipe } from 'effect';
const prompt = Prompt.make([
{ role: 'system', content: 'You are helpful.' },
{ role: 'user', content: [{ type: 'text', text: 'Hi' }] }
]);
const replaced = pipe(
prompt,
Prompt.setSystem('You are an expert in TypeScript.')
);
const prepended = pipe(prompt, Prompt.prependSystem('IMPORTANT: '));
const appended = pipe(prompt, Prompt.appendSystem(' Be concise.'));
History Management
Convert AI Response to Prompt
import * as Response from 'effect/unstable/ai/Response';
const responseParts: ReadonlyArray<Response.AnyPart> = [
Response.makePart('text-start', { id: 'text_1' }),
Response.makePart('text-delta', { id: 'text_1', delta: 'Hello' }),
Response.makePart('text-delta', { id: 'text_1', delta: '!' }),
Response.makePart('text-end', { id: 'text_1' }),
Response.makePart('tool-call', {
id: 'call_1',
name: 'get_time',
params: {},
providerExecuted: false
}),
Response.makePart('tool-approval-request', {
approvalId: 'approval_1',
toolCallId: 'call_1'
}),
Response.makePart('tool-result', {
id: 'call_1',
name: 'get_time',
isFailure: false,
result: '10:30 AM',
encodedResult: '10:30 AM',
providerExecuted: false,
preliminary: false
})
];
const historyPrompt = Prompt.fromResponseParts(responseParts);
Prompt.fromResponseParts folds streaming text/reasoning only when the matching start/delta/end parts are present in the same input, places tool calls and approval requests in assistant messages, places non-preliminary tool results in tool messages using encodedResult, and skips preliminary tool results.
Effect-Returning Prompt/Message Helpers
Keep pure prompt helpers pure. But when prompt or message transformation depends on services, model metadata, truncation policy, storage, or other runtime state, wrap the transformation in Effect.fn(...) so orchestrators can yield* it directly.
import { Effect } from 'effect';
export const toModelMessagesEffect = Effect.fn('Prompt.toModelMessages')(
function* (messages: ReadonlyArray<AppMessage>) {
const policy = yield* MessagePolicy.Service;
return convertMessages(messages, policy);
}
);
This keeps prompt assembly inside the Effect graph instead of forcing Promise islands through session orchestration code.
Typical Chat Pattern
import { Effect } from 'effect';
import * as SubscriptionRef from 'effect/SubscriptionRef';
import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
const chat = Effect.gen(function* () {
const history = yield* SubscriptionRef.make(Prompt.empty);
function* generateText(userInput: string) {
const currentHistory = yield* SubscriptionRef.get(history);
const prompt = pipe(currentHistory, Prompt.concat(userInput));
const response = yield* LanguageModel.generateText({ prompt });
const newHistory = pipe(
prompt,
Prompt.concat(Prompt.fromResponseParts(response.content))
);
yield* SubscriptionRef.set(history, newHistory);
return response;
}
return { generateText };
});
Usage with LanguageModel
Generate Text
import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
import { Effect } from 'effect';
const program = Effect.gen(function* () {
const prompt = Prompt.make([
{ role: 'system', content: 'You are helpful.' },
{ role: 'user', content: [{ type: 'text', text: 'Explain Effect' }] }
]);
const response = yield* LanguageModel.generateText({ prompt });
return response.content;
});
Stream Text
import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
import { Effect, Stream } from 'effect';
const program = Effect.gen(function* () {
const prompt = Prompt.make('Write a story');
yield* LanguageModel.streamText({ prompt }).pipe(
Stream.runForEach((part) =>
part.type === 'text-delta'
? Effect.sync(() => process.stdout.write(part.delta))
: Effect.void
)
);
});
Generate Object
import * as LanguageModel from 'effect/unstable/ai/LanguageModel';
import { Effect, Schema } from 'effect';
const Contact = Schema.Struct({
name: Schema.String,
email: Schema.String,
phone: Schema.optional(Schema.String)
});
const program = Effect.gen(function* () {
const prompt = Prompt.make('Extract: John Doe, john@example.com, 555-1234');
const response = yield* LanguageModel.generateObject({
prompt,
schema: Contact
});
return response.value;
});
Provider-Specific Options
declare module 'effect/unstable/ai/Prompt' {
interface TextPartOptions {
readonly anthropic?: {
readonly cache_control?: {
readonly type: 'ephemeral';
};
};
}
}
const cachedPart = Prompt.makePart('text', {
text: 'Large document...',
options: {
anthropic: { cache_control: { type: 'ephemeral' } }
}
});
const cachedMessage = Prompt.makeMessage('system', {
content: 'You are an expert.',
options: {
anthropic: { cache_control: { type: 'ephemeral' } }
}
});
Serialization
Export/Import
import * as Schema from 'effect/Schema';
import { Effect } from 'effect';
const encode = Schema.encodeEffect(Prompt.Prompt);
const decode = Schema.decodeUnknownEffect(Prompt.Prompt);
const program = Effect.gen(function* () {
const prompt = Prompt.make('Hello');
const exported = yield* encode(prompt);
const imported = yield* decode(exported);
return imported;
});
JSON Serialization
Use Schema.fromJsonString with Prompt.Prompt to create a JSON codec:
import * as Schema from 'effect/Schema';
import { Effect } from 'effect';
const PromptJson = Schema.fromJsonString(Prompt.Prompt);
const encodeJson = Schema.encodeEffect(PromptJson);
const decodeJson = Schema.decodeUnknownEffect(PromptJson);
const program = Effect.gen(function* () {
const prompt = Prompt.make([
{ role: 'system', content: 'You are helpful.' },
{ role: 'user', content: [{ type: 'text', text: 'Hi' }] }
]);
const json = yield* encodeJson(prompt);
yield* Effect.log(json);
const restored = yield* decodeJson(json);
return restored;
});
Note: There is no Prompt.FromJson in v4. Use Schema.fromJsonString(Prompt.Prompt) instead.
Common Patterns
Multi-turn Conversation
const conversation = Prompt.make([
{ role: 'system', content: 'You are a math tutor.' },
{ role: 'user', content: [{ type: 'text', text: 'What is 2+2?' }] },
{ role: 'assistant', content: [{ type: 'text', text: '2+2 equals 4.' }] },
{ role: 'user', content: [{ type: 'text', text: 'What about 3+3?' }] }
]);
Dynamic System Prompt
import { pipe } from 'effect';
function withSystemPrompt(content: string) {
return (prompt: Prompt.Prompt) => pipe(prompt, Prompt.setSystem(content));
}
const userPrompt = Prompt.make('Help me code');
const withContext = pipe(
userPrompt,
withSystemPrompt('You are an expert TypeScript developer.')
);
Tool Interaction History
const toolInteraction = Prompt.make([
{ role: 'user', content: [{ type: 'text', text: "What's the weather?" }] },
{
role: 'assistant',
content: [
{
type: 'tool-call',
id: '1',
name: 'weather',
params: {},
providerExecuted: false
}
]
},
{
role: 'tool',
content: [
{
type: 'tool-result',
id: '1',
name: 'weather',
isFailure: false,
result: 'Sunny, 22°C'
}
]
},
{
role: 'assistant',
content: [{ type: 'text', text: "It's sunny and 22°C." }]
}
]);
Type Guards
import * as Prompt from 'effect/unstable/ai/Prompt';
declare const value: unknown;
if (Prompt.isPrompt(value)) {
value.content;
}
if (Prompt.isMessage(value)) {
value.role;
}
if (Prompt.isPart(value)) {
value.type;
}
Anti-Patterns
const bad = {
role: 'user',
content: [{ type: 'text', text: 'Hello' }]
} as Prompt.UserMessage;
const good = Prompt.makeMessage('user', {
content: [Prompt.makePart('text', { text: 'Hello' })]
});
const better = Prompt.userMessage({
content: [Prompt.makePart('text', { text: 'Hello' })]
});
const prompt = Prompt.make('Hi');
prompt.content.push(someMessage);
const extended = Prompt.concat(prompt, 'Additional message');
const filtered = responseParts.filter((p) => p.type === 'text');
const historyPrompt = Prompt.fromResponseParts(responseParts);
Related Skills
- effect-ai-language-model - Using prompts with LanguageModel service
- effect-ai-streaming - Converting streaming responses to prompt history
- effect-ai-tool - Integrating tool call/result messages
Quality Checklist