--- url: 'https://liminal.land/effects.md' --- # Effect Primer ## Effect Overview [The effect library](https://effect.website) is a comprehensive toolkit and functional effect system for TypeScript. It provides a foundational building block, [the `Effect` type](https://effect.website/docs/getting-started/the-effect-type/), with which we compose our programs. Liminal exposes effects and effect factories specifically geared towards conversation management. The Effect runtime then interprets our program, thereby progressing the underlying conversation state. ## Runtime Requirements One of the driving motivations to model programs as Effects is dependency injection of runtime requirements. In an effect program, we can create a context tag, which represents a value that the program will ultimately need in order to execute. For example, we can define a `Random` context tag, which will allow us to decouple our random number factory implementation from its usage. <<< @/\_blocks/effect\_di.ts This effect capability provides the foundation for model-agnosticism as well as conversation state shadowing subtrees of our program. ## Model-agnosticism Liminal uses Effect AI's `AiLanguageModel` tag in order to keep Liminal effects decoupled from any language model or provider. Under the hood, Liminal effects yield the tag to implement against a single interface that represents the shared capabilities of different language models. ```ts twoslash // @noErrors import { Effect } from "effect" // ---cut--- import { AiLanguageModel } from "@effect/ai/AiLanguageModel" Effect.gen(function*() { const lm = yield* AiLanguageModel lm. // ^| }) ``` ## Conversations as Context Liminal also uses Effect's dependency injection to provide the current the [thread](/threads) (a conversation isolate), to subtrees of your program. When we `yield*` message effects, they retrieve and operate on the current conversation from the fiber context. ```ts twoslash import { Effect } from "effect" import L from "liminal" // ---cut--- const conversation = Effect.gen(function*() { yield* L.user`...` yield* L.assistant yield* L.user`...` yield* L.assistant }) ``` We provide threads (represented as Effect [layers](https://effect.website/docs/requirements-management/layers/)) to denote the boundary of the conversation. <<< @/\_blocks/conversation\_boundaries.ts *** Lets further-explore the behavior of threads in the next chapter. --- --- url: 'https://liminal.land/entity.md' --- # Entity --- --- url: 'https://liminal.land/events.md' --- # Liminal Events ## Event Format ```ts // ---cut--- import type { LEvent } from "liminal" declare const lEvent: LEvent // ^? ``` ## `L.events` <<< @/\_blocks/event\_logger.ts --- --- url: 'https://liminal.land/messages.md' --- # Liminal Messages ## Effect AI `Message` Format Liminal represents messages using Effect AI's provider-agnostic `Message` schema. ```ts import { Message } from "@effect/ai/AiInput" Message // ^? ``` ## `L.messages` To access the messages of the thread, use `L.messages`. ```ts import { Effect } from "effect" import L from "liminal" // ---cut--- Effect.gen(function*() { const messages = yield* L.messages // ^? }) ``` ## Serde We can use the Message schema to encode, persist and decode messages. This encoding/decoding handles various message part types, including images and file parts. <<< @/\_blocks/message\_e2e.ts ## `L.user` Append a user message to the thread. ```ts import { Effect, Schema } from "effect" import L from "liminal" // ---cut--- Effect.gen(function*() { // As a tagged template function call. yield* L.user`...` // As an ordinary function call. yield* L.user("...") }) ``` ## `L.userJson` Append the stringified JSON-serializable value to the thread. Optionally provide a schema, the annotations of which will be added as JSONC comments to the resulting JSON string contained within the new message. ```ts {5-9} import { Effect, Schema } from "effect" import L from "liminal" Effect.gen(function*() { yield* L.userJson({ outer: { inner: "value", }, }) }) ``` We can optionally pass a schema with description annotations, which will then be used to JSONC-encode the JSON with descriptions about corresponding values. ```ts import { Array, Console, Effect, Schema } from "effect" import L, { LPretty } from "liminal" const ExampleSchema = Schema.Struct({ inner: Schema.String.pipe( Schema.annotations({ description: "Some description for the LLM.", }), ), }) Effect.gen(function*() { yield* L.userJson({ inner: "value" }, ExampleSchema) }) ``` The resulting message looks as follows. ````txt ```jsonc { // Some description for the LLM. inner: "value" } ``` ```` ## `L.assistant` Infer a message from the model and append it to the thread. ```ts {4} import { Effect, Schema } from "effect" import L from "liminal" // ---cut--- Effect.gen(function*() { yield* L.user`...` const reply = yield* L.assistant reply satisfies string }) ``` ## `L.assistantSchema` Use Effect Schema to describe structured output requirements. ### Providing Schemas ```ts {4} twoslash import { Effect, Schema } from "effect" import L from "liminal" // ---cut--- Effect.gen(function*() { yield* L.user`Is Halloween the best holiday?` const result = yield* L.assistantSchema(Schema.Boolean) result satisfies boolean }) ``` We could of course also provide more complex structures, such as structs. ```ts {4-9} twoslash import { Effect, Schema } from "effect" import L from "liminal" // ---cut--- Effect.gen(function*() { yield* L.user`When is halloween?` const result = yield* L.assistantSchema( Schema.Struct({ month: Schema.Int, day: Schema.Int, }), ) result satisfies { month: number day: number } }) ``` ### Providing Field Schemas In the case of providing structs inline, we can skip the outer `Schema.Struct` wrapping, and directly pass the fields. ```ts {4-7} import { Effect, Schema } from "effect" import L from "liminal" // ---cut--- Effect.gen(function*() { yield* L.user`When is halloween?` const result = yield* L.assistantSchema({ month: Schema.Int, day: Schema.Int, }) result satisfies { month: number; day: number } }) ``` ## `L.clear` Clear the thread. ```ts {11} import { Effect } from "effect" import L from "liminal" declare const assertEquals: (a: unknown, b: unknown) => void // ---cut--- Effect.gen(function*() { // Initial messages. yield* L.user`A` yield* L.user`B` yield* L.user`C` // Clear the messages. yield* L.clear // The thread is now empty. const messages = yield* L.messages assertEquals(messages, []) }) ``` ## `L.append` Append raw Effect AI messages to the thread. <<< @/\_blocks/raw\_append.ts {7,10-16} --- --- url: 'https://liminal.land/overview.md' --- # Liminal Overview **Liminal enables the expression of reusable conversations as [effects](/effects.md).** <<< @/\_blocks/a\_taste.ts ## State Management We `yield*` Liminal effects to manage the underlying conversation state. The final conversation of the previous example may look as follows. ```json [ { "role": "system", "content": "You are a Leprechaun." }, { "role": "user", "content": "Where is the pot of gold?" }, { "role": "assistant", "content": "Under the rainbow." } ] ``` ## Unifying Conversation and Control Flow We reason about the progression of the conversation as one with ordinary function control flow. We can utilize ordinary control flow operators, such as `for` and `while` loops . <<< @/\_blocks/while\_looping.ts ## Reusable Patterns We can compose patterns that abstract over the low-level back-and-forth of user-assistant messaging. Conversations become our units of composition; we no longer think solely in terms of messages. For example, we can express a reusable iterative refinement conversation pattern as follows. `refine.ts` <<< @/\_blocks/refine.ts We can then share and consume this pattern––or any Liminal effect––as we would any other TypeScript file or library. `refine_consumer.ts` <<< @/\_blocks/refine\_consumer.ts ## Branching Liminal provides mechanisms for branching conversations so that we can easily explore alternative outcomes from any given state. In the following example, the `Rap`, `Rock` and `Pop` branches all inherit an isolated copy of the parent's messages, which contain the initial user message. <<< @/\_blocks/song\_branches.ts *** In the next section we cover Liminal's installation and a basic example of its usage. --- --- url: 'https://liminal.land/start.md' --- # Liminal Quickstart ## Installation Install `liminal` with your JavaScript package manager of choice. ::: code-group ```bash [npm] npm i liminal ``` ```bash [bun] bun i liminal ``` ```bash [deno] deno add npm:liminal ``` ```bash [pnpm] pnpm i liminal ``` ```bash [yarn] yarn install liminal ``` ::: > \[!NOTE] > Depending on whether you package manager auto-installs peer dependencies, you > may need to also install `effect`, `@effect/platform` and `@effect/ai`. Additionally, install [an Effect AI model provider](https://effect.website/docs/ai/introduction/#packages), which we'll use to execute our conversations. ::: code-group ```bash [npm] npm install @effect/ai-openai ``` ```bash [bun] bun install @effect/ai-openai ``` ```bash [deno] deno add @effect/ai-openai ``` ```bash [pnpm] pnpm @effect/ai-openai ``` ```bash [yarn] yarn add @effect/ai-openai ``` ::: ## Conversation Effects Liminal's effects and effect factories are accessible from the `L` namespace. ```ts twoslash // @noErrors import L from "liminal" L. //^| ``` We yield Liminal Effects within an `Effect.gen` body to control the underlying conversation state without manually managing structures the list of messages. <<< @/\_blocks/messaging\_101.ts ## Example Use Case Let's consider a function that validates an email address and returns either a validation error message or undefined if the supplies address is valid. <<< @/\_blocks/validateEmail\_initial.ts The error message we return is opaque; the caller lacks information about why validation failed. Let's use Liminal to infer a helpful validation error message. Also note how we `pipe` the effect into `L.thread` to mark the boundary of the conversation. <<< @/\_blocks/validateEmail.ts{7,10,13,15} ## Configuring Model Layer Some Liminal effects require a language model to specified. This is provided using the Effect AI `AiLanguageModel` tag. Let's create a layer to provide an OpenAI `AiLanguageModel`. `ModelLive.ts` <<< @/\_blocks/ModelLive.ts ## Running the Conversation We can now provide the `model` layer to any effect's we're ready to execute. You may want to satisfy requirements once at the root of your effect program. Alternatively, you can use it in the leaves of your program. <<< @/\_blocks/validateEmail\_run.ts {5-6} If the supplied email address is invalid, we may get error messages similar to the following. * `john..doe@example`: Your email is missing the top-level domain (like .com or .org) after 'example'. * `user@domain`: Your email address is incomplete and missing the domain extension. * `john@example..com`: Your email contains consecutive dots which aren't allowed in a valid address. *** In the next chapter, we touch on key concepts surrounding Liminal's implementation and usage. --- --- url: 'https://liminal.land/streaming.md' --- # Liminal Streaming <<< @/\_blocks/streaming.ts --- --- url: 'https://liminal.land/threads.md' --- # Liminal Threads ## `Thread` Overview A thread is a container of several properties: 1. A option of a system instruction. 2. A list of model-agnostic messages. The messages can be of varying role and content part types. 3. A set of enabled tool kits with which the model can perform tool-calling. 4. A pubsub with which we can subscribe to conversation events such as messaging. ## `L.thread` `L.thread` isolates the target effect with a new thread containing an empty message list and no system instruction nor tools. ```ts twoslash import { Effect } from "effect" import L from "liminal" declare const conversation: Effect.Effect // ---cut--- L.thread(conversation) ``` ## `L.branch` `L.branch` isolates the target effect with a new thread containing a copy of parent's messages, tools and system instruction. ```ts twoslash import { Effect } from "effect" import L from "liminal" declare const conversation: Effect.Effect // ---cut--- Effect.gen(function*() { yield* L.user`...` yield* L.assistant yield* conversation.pipe(L.branch) }) ``` --- --- url: 'https://liminal.land/tools.md' --- # Liminal Tools <<< @/\_blocks/DadJokeTools.ts <<< @/\_blocks/tool\_consumer.ts --- --- url: 'https://liminal.land/patterns/model.md' --- # Model Amalgamation --- --- url: 'https://liminal.land/patterns/route.md' --- # Route ## `Route.describe`