These are complete, copy-paste executors for the SDKs teams reach for most. Each wraps the SDK directly with createExecutor from @agentmark-ai/prompt-core — there’s no AgentMark adapter package to install or version to track. You own the code: copy the closest one, drop it in, adjust the model mapping, and you’re done.
Every executor here takes the neutral render (formatted.text_config.model_name, formatted.messages, and for object prompts formatted.object_config.schema) and returns { text | object, usage }. The builder turns that into AgentMark’s wire protocol for you. If your SDK isn’t listed, the shape is identical — see Connect your SDK for the full guide.
Vercel AI SDK
Wraps the ai package’s generateText / streamText (and generateObject for structured output). This example does text with both one-shot and streaming paths:
import { createExecutor } from "@agentmark-ai/prompt-core";
import { generateText, streamText, generateObject, jsonSchema } from "ai";
import { openai } from "@ai-sdk/openai";
// Minimal model resolution — strip an optional "openai/" prefix. Swap in your
// own provider map (anthropic, google, …) keyed off the model name as needed.
const model = (name: string) => openai(name.replace(/^openai\//, ""));
export const executor = createExecutor({
name: "vercel-ai-sdk",
text: async (formatted, ctx) => {
const { text, usage } = await generateText({
model: model(formatted.text_config.model_name),
messages: formatted.messages,
abortSignal: ctx.signal,
});
// AI SDK v5 usage is already canonical ({ inputTokens, outputTokens }).
// On v4 it's { promptTokens, completionTokens } — rename those two.
return { text, usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens } };
},
streamText: async function* (formatted, ctx) {
const result = streamText({
model: model(formatted.text_config.model_name),
messages: formatted.messages,
abortSignal: ctx.signal,
});
for await (const delta of result.textStream) yield { type: "text-delta", text: delta };
const usage = await result.usage;
yield { type: "finish", reason: "stop", usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens } };
},
object: async (formatted, ctx) => {
const { object, usage } = await generateObject({
model: model(formatted.object_config.model_name),
messages: formatted.messages,
schema: jsonSchema(formatted.object_config.schema),
abortSignal: ctx.signal,
});
return { object, usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens } };
},
});
OpenAI (raw SDK)
The official openai package — chat.completions.create. Messages map straight through; structured output uses JSON-schema response format:
import { createExecutor } from "@agentmark-ai/prompt-core";
import OpenAI from "openai";
const openai = new OpenAI();
const model = (name: string) => name.replace(/^openai\//, "");
export const executor = createExecutor({
name: "openai",
text: async (formatted, ctx) => {
const res = await openai.chat.completions.create(
{ model: model(formatted.text_config.model_name), messages: formatted.messages },
{ signal: ctx.signal },
);
return {
text: res.choices[0].message.content ?? "",
finishReason: res.choices[0].finish_reason,
usage: { inputTokens: res.usage?.prompt_tokens ?? 0, outputTokens: res.usage?.completion_tokens ?? 0 },
};
},
object: async (formatted, ctx) => {
const res = await openai.chat.completions.create(
{
model: model(formatted.object_config.model_name),
messages: formatted.messages,
response_format: {
type: "json_schema",
json_schema: { name: "response", schema: formatted.object_config.schema, strict: true },
},
},
{ signal: ctx.signal },
);
return {
object: JSON.parse(res.choices[0].message.content ?? "{}"),
usage: { inputTokens: res.usage?.prompt_tokens ?? 0, outputTokens: res.usage?.completion_tokens ?? 0 },
};
},
});
Anthropic (raw SDK)
The @anthropic-ai/sdk messages.create. Anthropic takes system as a top-level field and requires max_tokens, so split the system message out of the neutral render:
import { createExecutor } from "@agentmark-ai/prompt-core";
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();
const model = (name: string) => name.replace(/^anthropic\//, "");
export const executor = createExecutor({
name: "anthropic",
text: async (formatted, ctx) => {
const system = formatted.messages.filter((m) => m.role === "system").map((m) => m.content).join("\n");
const messages = formatted.messages.filter((m) => m.role !== "system");
const res = await anthropic.messages.create(
{
model: model(formatted.text_config.model_name),
max_tokens: formatted.text_config.max_tokens ?? 1024,
system,
messages: messages as Anthropic.MessageParam[],
},
{ signal: ctx.signal },
);
const text = res.content.map((b) => (b.type === "text" ? b.text : "")).join("");
return { text, usage: { inputTokens: res.usage.input_tokens, outputTokens: res.usage.output_tokens } };
},
});
Python (OpenAI)
The same shape in Python: a create_executor handler returning an ExecutorTextResult with a canonical UsageData.
from agentmark.prompt_core import create_executor, ExecutorTextResult, UsageData
from openai import OpenAI
client = OpenAI()
def _model(name: str) -> str:
return name.removeprefix("openai/")
def _text(formatted, ctx) -> ExecutorTextResult:
res = client.chat.completions.create(
model=_model(formatted["text_config"]["model_name"]),
messages=formatted["messages"],
)
usage = res.usage
return ExecutorTextResult(
text=res.choices[0].message.content or "",
usage=UsageData(input_tokens=usage.prompt_tokens, output_tokens=usage.completion_tokens),
)
executor = create_executor(name="openai", text=_text)
Pass object= for structured output and stream_text= / stream_object= async generators for streaming — the same handler set as TypeScript. See Connect your SDK for the streaming and object handlers.
Agent frameworks (Pydantic AI, Mastra, Claude Agent SDK)
Agent frameworks follow the identical shape — the only difference is that your handler runs an agent loop instead of a single completion. Feed the neutral render’s messages into your agent, run it, and return its final output plus token usage:
import { createExecutor } from "@agentmark-ai/prompt-core";
export const executor = createExecutor({
name: "my-agent-framework",
text: async (formatted, ctx) => {
// Build + run your agent (Pydantic AI Agent.run, a Mastra Agent, Claude's
// query(), …) with formatted.messages, honoring ctx.signal for cancellation.
const result = await runMyAgent(formatted.messages, { signal: ctx.signal });
return { text: result.output, usage: { inputTokens: result.inputTokens, outputTokens: result.outputTokens } };
},
});
For token-by-token output and tool-call/tool-result events, use a streamText handler and yield text-delta / tool-call / tool-result events as the agent emits them — see the streaming section of Connect your SDK.
Serve it
Every executor above plugs into the same wiring. Build a runner, register your evals there (so they both run in experiments and list in the New Experiment dialog), and the deployed managed handler is one line:
import { createWebhookRunner } from "@agentmark-ai/sdk";
const runner = createWebhookRunner({ executor, loader, evals: myEvals });
export default (body) => runner.dispatch(body);
See Connect your SDK, Path B for serving locally vs. a managed deploy.
Validate whatever you build
Gate any executor — copied or hand-written — with the conformance suite. One call confirms a protocol-correct stream for every kind, streaming and one-shot, including the error path:
import { runExecutorConformance } from "@agentmark-ai/prompt-core";
await runExecutorConformance(executor, {
text: { messages: [{ role: "user", content: "hello" }], text_config: { model_name: "openai/gpt-4o" } },
object: { messages: [{ role: "user", content: "give me JSON" }], object_config: { model_name: "openai/gpt-4o" } },
errorInput: { messages: null },
});
See Validate your executor for the Python equivalent and the full pattern.
Have Questions?
We’re here to help! Choose the best way to reach us: