Ploy
Ploy
Agent SDK

createAgent()

Configure and create an AI agent with a single function call.

createAgent()

createAgent(config) is the main entry point of the Agent SDK. It takes an AgentConfig and returns a Ploy<PloyEnv> export that handles fetch requests, timer events, and the durable agent workflow.

AgentConfig

import { createAgent } from "@meetploy/agent-sdk";

export default createAgent({
	// Required
	systemPrompt: "You are a helpful assistant.",
	handle: myHandler,

	// Optional
	model: "auto",
	contextCompression: {
		enabled: true,
		maxTokens: 2000,
		summaryModel: "gpt-4o-mini",
		preserveRecentMessages: 12,
	},
	streaming: { enabled: false },
	features: { memory: true, artifacts: true, scheduling: true, mcp: true },
	tools: [myTool],
	hooks: {
		onRunPartial(ctx, partial) {
			/* ... */
		},
		onRunComplete(ctx, response) {
			/* ... */
		},
	},
	maxSteps: 30,
});

systemPrompt

The system prompt sent to the AI on every turn. Can be a static string or a function that returns a per-user prompt:

// Static
systemPrompt: "You are a helpful assistant.";

// Dynamic per-user
systemPrompt: async (userId) => {
	const prefs = await loadPreferences(userId);
	return `You are a helpful assistant. Language: ${prefs.language}.`;
};

The SDK automatically appends the user's stored memories to the system prompt. When context compression is enabled, it also appends the running conversation summary.

handle

Your custom request handler. This is where you parse incoming requests (HTTP, webhooks, etc.), call agent.handleMessage(), and return a response:

async function handle(req: Request, env: PloyEnv, agent: AgentContext) {
	const body = (await req.json()) as {
		userId: string;
		chatId: string;
		text: string;
	};

	const messenger = new MyMessenger();

	await agent.handleMessage({
		userId: body.userId,
		chatId: body.chatId,
		platform: "http",
		text: body.text,
		messenger,
	});

	return Response.json({ ok: true });
}

The AgentContext passed to your handler provides:

PropertyTypeDescription
handleMessage(params) => Promise<void>Enqueue a user message for processing
commandsAgentCommandsBuilt-in commands (reset, debug, tools, MCP)
stateStateManagerDirect access to the state binding
envPloyEnvThe full Ploy environment bindings

model

The model name passed to the AI gateway. Defaults to "auto".

model: "gpt-4o";

contextCompression

Enable summary-based context compression for long-running conversations:

contextCompression: {
	enabled: true,
	maxTokens: 2000,
	summaryModel: "gpt-4o-mini",
	preserveRecentMessages: 12,
}

Options:

PropertyTypeDescription
enabledbooleanEnables summary-based compression. Disabled by default.
maxTokensnumberEstimated prompt budget before older history is summarized.
summaryModelstringOptional model used for summary generation. Defaults to the main model.
preserveRecentMessagesnumberNumber of most recent chat messages to keep verbatim. Defaults to 12.
summaryPromptstringOptional prompt template for the summarizer. Supports {summary} and {messages} placeholders.

When the estimated prompt size exceeds maxTokens, the SDK summarizes older history, stores the result in the user's workspace, and keeps only the most recent messages verbatim. That summary is then injected into future system prompts.

This is currently based on estimated prompt size, not exact tokenizer counts. The API is stable, but the threshold is approximate until the SDK grows native tokenizer support.

features

Toggle built-in tool categories. All default to true:

features: {
  memory: true,      // memory_set, memory_get, memory_delete, memory_list
  artifacts: true,   // artifact_create
  scheduling: true,  // schedule_task, list_scheduled_tasks, get_scheduled_task, update_scheduled_task, delete_scheduled_task
  mcp: true,         // mcp_call_tool (when MCP servers are installed)
}

streaming

Enable first-class partial generation inside the SDK workflow:

streaming: {
	enabled: true,
	throttleMs: 400,
	minCharsDelta: 24,
	maxPartialLength: 4096,
}

When enabled, the SDK requests stream: true from the AI gateway, emits throttled partial updates to hooks.onRunPartial during generation, and still persists only the final assistant message into workspace history.

Because the durable workflow does not carry a live runtime messenger instance today, streamed partials are currently consumed through hooks. The Messenger.sendPartialMessage method is available for transports or adapters that can bridge partial delivery themselves.

Example:

import { createAgent } from "@meetploy/agent-sdk";

export default createAgent({
	systemPrompt: "You are a helpful assistant.",
	handle: myHandler,
	streaming: {
		enabled: true,
		throttleMs: 250,
		minCharsDelta: 12,
	},
	hooks: {
		async onRunPartial(ctx, partial) {
			await ctx.state.setJSON(`draft:${ctx.chatId}`, {
				key: partial.key,
				text: partial.text,
				step: partial.step,
				isFinal: partial.isFinal,
			});
		},
		async onRunComplete(ctx, response) {
			await ctx.state.delete(`draft:${ctx.chatId}`);
			await ctx.state.set(`last_response:${ctx.chatId}`, response);
		},
	},
});

In that flow, onRunPartial receives the current accumulated assistant text as it grows. partial.key stays stable for the in-progress assistant turn, so your UI or adapter can replace the same draft message instead of appending a new one on every update.

tools

Array of custom tool definitions. Use defineTool() for type-safe args:

import { defineTool } from "@meetploy/agent-sdk";

const myTool = defineTool({
	name: "lookup_user",
	description: "Look up a user by ID",
	parameters: {
		type: "object",
		properties: { userId: { type: "string" } },
		required: ["userId"],
	},
	async execute(args, ctx) {
		// args.userId is typed as string
		const data = await ctx.state.get(`user:${args.userId}`);
		return data ?? "User not found";
	},
});

See Tools for full documentation.

hooks

Lifecycle hooks that fire during agent execution. Every hook receives a HookContext with access to env, state, and an execute() function for calling tools:

hooks: {
  async onRunComplete(ctx, response) {
    await ctx.state.set(`last_response:${ctx.userId}`, response);
  },
  async onRunError(ctx, error) {
    console.error(`Agent error for ${ctx.userId}: ${error.message}`);
  },
}

See Hooks for the full list.

maxSteps

Maximum iterations of the AI-call-then-tool-execution loop per run. Prevents runaway tool loops. Defaults to 30.

maxSteps: 50;

Messenger Interface

The Messenger you pass to handleMessage() defines how the agent sends responses back to the user. Implement it for your platform:

import type { Messenger } from "@meetploy/agent-sdk";

class TelegramMessenger implements Messenger {
	sendMessage(chatId: string, text: string) {
		return fetch(`https://api.telegram.org/bot${token}/sendMessage`, {
			method: "POST",
			body: JSON.stringify({ chat_id: chatId, text }),
		}).then(() => undefined);
	}

	sendDocument(chatId: string, filename: string, content: string | Uint8Array) {
		// Upload file to Telegram
	}

	sendTypingIndicator(chatId: string) {
		return fetch(`https://api.telegram.org/bot${token}/sendChatAction`, {
			method: "POST",
			body: JSON.stringify({ chat_id: chatId, action: "typing" }),
		}).then(() => undefined);
	}

	// Optional for transports that can bridge partial delivery outside
	// the durable workflow. The SDK's built-in streaming flow currently
	// emits partials through hooks.onRunPartial.
	sendPartialMessage?(chatId: string, partial: PartialMessage) {
		return updateDraft(chatId, partial.key, partial.text);
	}
}

Return Value

createAgent() returns a Ploy<PloyEnv> object with:

  • fetch -- Handles HTTP requests (health check + delegates to your handle function)
  • timer -- Handles scheduled task triggers
  • workflows.ploy_agent_run -- The durable agent tick loop

The SDK owns the timer and workflows handlers. You do not need to define these yourself -- only implement the handle function.

Next Steps

How is this guide?

Last updated on