Ploy
Ploy
Agent SDK

State & Workspaces

Per-user workspace management and the StateManager API.

State & Workspaces

The Agent SDK manages per-user state automatically through workspaces. Each user gets an isolated workspace containing their conversation history, memory entries, MCP server configs, and run state. The StateManager provides a typed wrapper over the raw Ploy StateBinding.

Workspaces

A workspace is created automatically when a user sends their first message. It's stored in the state binding under the key agent:workspace:{userId}.

interface Workspace {
	userId: string;
	chatId: string;
	platform: string;
	messages: ChatMessage[]; // Conversation history (last 50 messages)
	memory: Record<string, MemoryEntry>; // User memories (set via memory tools)
	mcpServers: McpServer[]; // Installed MCP servers
	debugMode: boolean; // Debug mode toggle
	activeRunId: string | null; // Active workflow run ID (null if idle)
	queue: QueuedMessage[]; // Messages queued during active runs
}

Automatic Behaviors

The SDK handles these workspace operations automatically:

  • Creation -- A default workspace is created on the user's first message
  • Message trimming -- Conversation history is capped at 50 messages to prevent unbounded growth
  • Memory injection -- Memory entries are appended to the system prompt (secrets are hidden)
  • Message queuing -- Messages received during an active run are queued and processed in order
  • Run tracking -- activeRunId prevents concurrent runs for the same user

StateManager

The StateManager wraps the raw StateBinding with JSON-aware helpers. It's available on AgentContext.state, ToolContext.state, and HookContext.state.

API

interface StateManager {
	get(key: string): Promise<string | null>;
	set(key: string, value: string): Promise<void>;
	delete(key: string): Promise<void>;
	getJSON<T>(key: string): Promise<T | null>;
	setJSON(key: string, value: unknown): Promise<void>;
}

Usage in handle()

handle: async (req, env, agent) => {
	// Read state directly
	const visits = (await agent.state.getJSON<number>("visits")) ?? 0;
	await agent.state.setJSON("visits", visits + 1);

	return Response.json({ visits: visits + 1 });
};

Usage in Tools

const myTool = defineTool({
	name: "save_note",
	description: "Save a note",
	parameters: {
		type: "object",
		properties: { text: { type: "string" } },
		required: ["text"],
	},
	async execute(args, ctx) {
		await ctx.state.set(`note:${ctx.userId}`, args.text);
		return "Note saved.";
	},
});

Usage in Hooks

hooks: {
  async onRunComplete(ctx, response) {
    const count = await ctx.state.getJSON<number>(`runs:${ctx.userId}`) ?? 0;
    await ctx.state.setJSON(`runs:${ctx.userId}`, count + 1);
  },
}

Memory Entries

Memory entries are key-value pairs stored in the workspace. The agent can set, get, delete, and list them using the built-in memory tools. Memories are automatically injected into the system prompt so the agent remembers user preferences across conversations.

interface MemoryEntry {
	value: string;
	secret: boolean; // Hidden in debug output and system prompt
	createdAt: number;
}

Secret memories (e.g., API keys) are injected with the value [SECRET] in the system prompt and debug views, but the full value is available to tools.

State values are strings. Use getJSON/setJSON for structured data. The SDK uses JSON.stringify/JSON.parse internally.

Next Steps

  • createAgent() -- The state property on AgentContext
  • Tools -- Access state via ctx.state in tool execute functions
  • Commands -- The /reset command deletes the user's workspace

How is this guide?

Last updated on