Build agents
LangChain.js
Any LangChain.js chat model answers in MorphScreen's format once it has MorphScreen's system prompt. The web SDK draws the text and its core checks every line as it streams.
npm install @langchain/core @langchain/openaipnpm add @langchain/core @langchain/openaiyarn add @langchain/core @langchain/openaibun add @langchain/core @langchain/openaiA LangChain model writes MorphScreen answers when it gets MorphScreen's system prompt. Your chain, tools and model stay as they are; MorphScreen adds the prompt on the way in and draws the text on the way out.
The prompt and the stream
// A LangChain.js model answering in MorphScreen's format: any chat model
// LangChain has, given MorphScreen's system prompt. The page draws the text
// with the web SDK, whose core checks each line as it arrives.
import { SystemMessage } from "@langchain/core/messages";
const API = process.env.MORPHSCREEN_API ?? "https://api.morphscreen.com/v1";
/** MorphScreen's system prompt for these packs. Fetch it once at start: it isn't counted as a call. */
export async function morphscreenPrompt(packs = ["data", "inputs", "interactive"]) {
const res = await fetch(`${API}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-MorphScreen-Key": process.env.MORPHSCREEN_KEY },
body: JSON.stringify({ packs }),
});
if (!res.ok) throw new Error(`MorphScreen ${res.status}: ${await res.text()}`);
return (await res.json()).prompt;
}
/** Yields the answer's text, piece by piece, from any LangChain chat model. */
export async function* streamAnswer(model, prompt, messages) {
for await (const chunk of await model.stream([new SystemMessage(prompt), ...messages])) {
if (typeof chunk.content === "string" && chunk.content) yield chunk.content;
}
}morphscreenPrompt calls POST /v1/prompt, which isn't counted as a call. Fetch it once when the server starts, with the packs your answers use.
With a model
Any chat model LangChain has works. On OpenRouter, for example:
import { ChatOpenAI } from "@langchain/openai";
import { morphscreenPrompt, streamAnswer } from "./morphscreen.mjs";
const model = new ChatOpenAI({
model: "deepseek/deepseek-v4.1-flash",
apiKey: process.env.OPENROUTER_API_KEY,
configuration: { baseURL: "https://openrouter.ai/api/v1" },
});
const prompt = await morphscreenPrompt(["data", "inputs", "interactive"]);
for await (const text of streamAnswer(model, prompt, [{ role: "user", content: "Compare three running shoes under $120" }])) {
// send each piece to the page
}In the page
Send each piece to the page as it comes (a streamed HTTP response or a WebSocket), and draw it with createAnswer or <Answer>. The core in the page reads each line against the components you allow as it arrives: an unknown component becomes plain text and a bad value falls back to its default (Validation and repair), and nothing the model wrote ever runs.
What you give up
Here your own model writes the answer, so MorphScreen's API isn't in the path and its repairs aren't either: a line with an error is drawn as well as the core can read it, not rewritten by the model. When you want repairs, send the conversation to /v1/chat/completions instead, on the model in your plan or your own OpenRouter key.
For a LangGraph agent served over AG-UI to connectAgent, see LangGraph.
We run morphscreen.mjs in CI: it fetches the prompt from the API, and a scripted LangChain model streams a recorded answer through streamAnswer.