effect-ai-prompt

SKILL.md

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/ai/Prompt"
import * as Response from "@effect/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

-- Composition
Prompt.make       :: RawInputPrompt
Prompt.merge      :: (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/ai/Prompt"

// String content only
const system = Prompt.makeMessage("system", {
  content: "You are a helpful assistant specialized in mathematics."
})

// Shorthand constructor
const systemShorthand = Prompt.systemMessage({
  content: "You are a helpful assistant specialized in mathematics."
})

// System message with options
const systemWithOptions = Prompt.makeMessage("system", {
  content: "You are an expert coder.",
  options: {
    "anthropic": { cache_control: { type: "ephemeral" } }
  }
})

User Messages

// Text-only user message
const userText = Prompt.makeMessage("user", {
  content: [
    Prompt.makePart("text", { text: "What is 2+2?" })
  ]
})

// Shorthand constructor
const userShorthand = Prompt.userMessage({
  content: [
    Prompt.makePart("text", { text: "What is 2+2?" })
  ]
})

// Multimodal user message (text + file)
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

// Assistant response with text and tool call
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("text", {
      text: "The weather in Paris is sunny, 22°C."
    })
  ]
})

// Shorthand constructor
const assistantShorthand = Prompt.assistantMessage({
  content: [
    Prompt.makePart("text", {
      text: "The weather in Paris is sunny, 22°C."
    })
  ]
})

Tool Messages

// Tool execution results
const toolMessage = Prompt.makeMessage("tool", {
  content: [
    Prompt.makePart("tool-result", {
      id: "call_123",
      name: "get_weather",
      isFailure: false,
      result: { temperature: 22, condition: "sunny" }
    })
  ]
})

// Shorthand constructor
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

// From URL
const fileFromUrl = Prompt.makePart("file", {
  mediaType: "image/png",
  fileName: "screenshot.png",
  data: new URL("https://example.com/image.png")
})

// From bytes
const fileFromBytes = Prompt.makePart("file", {
  mediaType: "application/pdf",
  fileName: "report.pdf",
  data: new Uint8Array([1, 2, 3])
})

// From base64
const fileFromBase64 = Prompt.makePart("file", {
  mediaType: "image/jpeg",
  data: "data:image/jpeg;base64,/9j/4AAQ..."
})

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
})

Prompt Construction

From String

// Creates a user message with text part
const prompt = Prompt.make("Hello, how are you?")

From Messages

const prompt = Prompt.make([
  { role: "system", content: "You are helpful." },
  { role: "user", content: [{ type: "text", text: "Hi!" }] }
])

From Existing Prompt

const copy = Prompt.make(existingPrompt)

Empty Prompt

const empty = Prompt.empty

From Messages Array

// Using fromMessages constructor
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)

// Alternative: Using make with messages array
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")

// Data-last (pipeline)
const combined = pipe(
  systemPrompt,
  Prompt.merge(userPrompt)
)

// Data-first
const combined2 = Prompt.merge(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" }] }
])

// Replace system message
const replaced = pipe(
  prompt,
  Prompt.setSystem("You are an expert in TypeScript.")
)

// Prepend to system message
const prepended = pipe(
  prompt,
  Prompt.prependSystem("IMPORTANT: ")
)
// Result: "IMPORTANT: You are helpful."

// Append to system message
const appended = pipe(
  prompt,
  Prompt.appendSystem(" Be concise.")
)
// Result: "You are helpful. Be concise."

History Management

Convert AI Response to Prompt

import * as Response from "@effect/ai/Response"

const responseParts: ReadonlyArray<Response.AnyPart> = [
  Response.makePart("text", { text: "Hello!" }),
  Response.makePart("tool-call", {
    id: "call_1",
    name: "get_time",
    params: {},
    providerExecuted: false
  }),
  Response.makePart("tool-result", {
    id: "call_1",
    name: "get_time",
    isFailure: false,
    result: "10:30 AM",
    encodedResult: "10:30 AM",
    providerExecuted: false
  })
]

// Converts to assistant + tool messages
const historyPrompt = Prompt.fromResponseParts(responseParts)

Typical Chat Pattern

import { Effect } from "effect"
import * as SubscriptionRef from "effect/SubscriptionRef"
import * as LanguageModel from "@effect/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.merge(userInput)
    )

    const response = yield* LanguageModel.generateText({ prompt })

    // Update history with user input + response
    const newHistory = pipe(
      prompt,
      Prompt.merge(Prompt.fromResponseParts(response.content))
    )
    yield* SubscriptionRef.set(history, newHistory)

    return response
  }

  return { generateText }
})

Usage with LanguageModel

Generate Text

import * as LanguageModel from "@effect/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/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/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.object
})

Provider-Specific Options

// Augment options interfaces via module augmentation
declare module "@effect/ai/Prompt" {
  interface TextPartOptions {
    readonly anthropic?: {
      readonly cache_control?: {
        readonly type: "ephemeral"
      }
    }
  }
}

// Use in parts
const cachedPart = Prompt.makePart("text", {
  text: "Large document...",
  options: {
    anthropic: { cache_control: { type: "ephemeral" } }
  }
})

// Use in messages
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.encode(Prompt.Prompt)
const decode = Schema.decodeUnknown(Prompt.Prompt)

const program = Effect.gen(function* () {
  const prompt = Prompt.make("Hello")

  // Export to structured data
  const exported = yield* encode(prompt)

  // Import from structured data
  const imported = yield* decode(exported)

  return imported
})

JSON Serialization

import * as Schema from "effect/Schema"
import { Effect } from "effect"

const encodeJson = Schema.encode(Prompt.FromJson)
const decodeJson = Schema.decodeUnknown(Prompt.FromJson)

const program = Effect.gen(function* () {
  const prompt = Prompt.make([
    { role: "system", content: "You are helpful." },
    { role: "user", content: [{ type: "text", text: "Hi" }] }
  ])

  // To JSON
  const json = yield* encodeJson(prompt)
  console.log(json) // string

  // From JSON
  const restored = yield* decodeJson(json)

  return restored
})

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/ai/Prompt"

declare const value: unknown

if (Prompt.isPrompt(value)) {
  // value: Prompt.Prompt
  console.log(value.content)
}

if (Prompt.isMessage(value)) {
  // value: Prompt.Message
  console.log(value.role)
}

if (Prompt.isPart(value)) {
  // value: Prompt.Part
  console.log(value.type)
}

Anti-Patterns

// DON'T: Manually construct messages without constructors
const bad = {
  role: "user",
  content: [{ type: "text", text: "Hello" }]
} as Prompt.UserMessage

// DO: Use constructors
const good = Prompt.makeMessage("user", {
  content: [Prompt.makePart("text", { text: "Hello" })]
})

// DO: Use shorthand constructors
const better = Prompt.userMessage({
  content: [Prompt.makePart("text", { text: "Hello" })]
})

// DON'T: Mutate prompt content
const prompt = Prompt.make("Hi")
prompt.content.push(someMessage) // Error: readonly

// DO: Use merge for composition
const extended = Prompt.merge(prompt, "Additional message")

// DON'T: Manually filter response parts
const filtered = responseParts.filter(p => p.type === "text")

// DO: Use fromResponseParts (handles streaming deltas, tool results, etc.)
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

  • Messages use Prompt.makeMessage or shorthand constructors
  • Parts use Prompt.makePart constructors
  • System messages managed with setSystem/prependSystem/appendSystem
  • History updates use Prompt.fromResponseParts
  • Prompt composition uses Prompt.merge (not mutation)
  • Namespace imports for all @effect/ai modules
Weekly Installs
7
GitHub Stars
16
First Seen
Jan 24, 2026
Installed on
claude-code5
codex5
gemini-cli5
opencode5
trae4
antigravity4