Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-waves-schedule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agents": minor
---

Extract Agent scheduling into the `AgentScheduler` lifecycle component and publish it from `agents/schedules`. Existing Agent scheduling methods remain compatible delegators, and the previous `agents/schedule` parser entry point remains available as a deprecated compatibility re-export.
40 changes: 37 additions & 3 deletions docs/agents/scheduling.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,40 @@ The scheduling system supports four modes:

Under the hood, scheduling uses [Durable Object alarms](https://developers.cloudflare.com/durable-objects/api/alarms/) to wake the agent at the right time. Tasks are stored in a SQLite table and executed in order.

## Scheduling capability

Every `Agent` has an `AgentScheduler` at `this.schedules`. It owns schedule storage, queries, retries, callback execution, and cleanup. The existing `this.schedule()`, `this.scheduleEvery()`, `this.getScheduleById()`, `this.listSchedules()`, and `this.cancelSchedule()` methods delegate to it, so existing Agent code does not need to change.

Import schedule types or the manager from the stable entry point:

```typescript
import {
AgentScheduler,
type Schedule,
type ScheduleCriteria
} from "agents/schedules";
```

Most agents should continue to use the Agent methods. To extend scheduling policy, replace the component in a subclass:

```typescript
import { Agent } from "agents";
import { AgentScheduler } from "agents/schedules";

class TracingScheduler extends AgentScheduler {
override async cancelSchedule(id: string): Promise<boolean> {
console.log("Cancelling schedule", id);
return super.cancelSchedule(id);
}
}

export class MyAgent extends Agent<Env> {
override schedules = new TracingScheduler(this);
}
```

The Agent still owns the single physical Durable Object alarm because schedules share it with keep-alive heartbeats, fiber recovery, facet runs, and timers. Replacing `schedules` changes schedule behavior without replacing that alarm arbitration.

## Quick Start

```typescript
Expand Down Expand Up @@ -642,14 +676,14 @@ class TimezoneAgent extends Agent {

## AI-Assisted Scheduling

The SDK includes utilities for parsing natural language scheduling requests with AI.
The SDK includes utilities for parsing natural language scheduling requests with AI. Import them from `agents/schedules`. The previous `agents/schedule` path remains available as a deprecated compatibility entry point.

### getSchedulePrompt()

Returns a system prompt for parsing natural language into scheduling parameters:

```typescript
import { getSchedulePrompt, scheduleSchema } from "agents";
import { getSchedulePrompt, scheduleSchema } from "agents/schedules";
import { generateObject } from "ai";
import { openai } from "@ai-sdk/openai";

Expand Down Expand Up @@ -703,7 +737,7 @@ class SmartScheduler extends Agent {
A Zod schema for validating parsed scheduling data:

```typescript
import { scheduleSchema } from "agents";
import { scheduleSchema } from "agents/schedules";

// The schema uses a discriminated union on `when.type`:
// {
Expand Down
8 changes: 5 additions & 3 deletions packages/agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ Each export maps to a public entry point that users `import` from. These are the
| `agents/mcp/client` | `src/mcp/client.ts` | MCP client manager (connect to remote MCP servers from an Agent) |
| `agents/email` | `src/email.ts` | Email routing, resolvers, header signing |
| `agents/workflows` | `src/workflows.ts` | `AgentWorkflow` — Workflows integrated with Agents |
| `agents/schedule` | `src/schedule.ts` | Scheduling types |
| `agents/schedules` | `src/schedules/` | `AgentScheduler`, runtime schedule types, and parsing helpers |
| `agents/schedule` | `src/schedule.ts` | Deprecated compatibility path for natural-language schedule parsing |
| `agents/observability` | `src/observability/index.ts` | Observability event types and emitters |
| `agents/ai-chat-agent` | `src/ai-chat-agent.ts` | Legacy AI chat agent (prefer `@cloudflare/ai-chat`) |
| `agents/ai-react` | `src/ai-react.tsx` | Legacy AI React hooks (prefer `@cloudflare/ai-chat`) |
Expand All @@ -40,7 +41,8 @@ src/
sub-routing.ts # Nested /sub/... routing helpers + getSubAgentByName
email.ts # Email routing utilities
workflows.ts # AgentWorkflow base class
schedule.ts # Scheduling types and helpers
schedule.ts # Deprecated compatibility barrel for schedule parsing
schedules/ # AgentScheduler, runtime types, schema, and parsing helpers
serializable.ts # RPC serialization types
types.ts # Shared message type enums
utils.ts # Helpers (camelCaseToKebabCase, etc.)
Expand Down Expand Up @@ -218,7 +220,7 @@ AI evaluation suite (scheduling accuracy, etc.). Requires API keys in `.env`.
- **State sync is bidirectional** — `this.setState()` on the server broadcasts to all connected clients; `agent.setState()` from the client sends to the server. Both directions use the same message format (`MessageType.CF_AGENT_STATE`).
- **RPC is reflection-based** — public methods on Agent subclasses are automatically callable from clients via `agent.call("methodName", ...args)`. Serialization constraints are enforced by the `Serializable` type system (`src/serializable.ts`).
- **Sub-agents are facets** — `subAgent(Cls, name)` creates or resolves a child DO colocated on the same machine. Clients reach a child via `/agents/{parent}/{name}/sub/{child}/{name}` and `useAgent({ sub: [...] })`. Parents gate access with `onBeforeSubAgent`; children reach their parent with `parentAgent(Cls)` or `parentPath`.
- **Scheduling uses cron-schedule** — `this.schedule()` accepts delays, Dates, or cron strings. Schedules persist in SQLite and survive hibernation.
- **Scheduling uses cron-schedule** — `this.schedule()` accepts delays, Dates, or cron strings. `AgentScheduler` owns schedule storage and execution while `Agent` retains the shared physical alarm arbitration. Schedules persist in SQLite and survive hibernation.
- **MCP has two sides** — `McpAgent` (in `mcp/index.ts`) lets you _build_ an MCP server. `MCPClientManager` (in `mcp/client.ts`) lets an Agent _connect to_ external MCP servers.

## Boundaries
Expand Down
5 changes: 5 additions & 0 deletions packages/agents/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,11 @@
"import": "./dist/schedule.js",
"require": "./dist/schedule.js"
},
"./schedules": {
"types": "./dist/schedules/index.d.ts",
"import": "./dist/schedules/index.js",
"require": "./dist/schedules/index.js"
},
"./workflows": {
"types": "./dist/workflows.d.ts",
"import": "./dist/workflows.js",
Expand Down
1 change: 1 addition & 0 deletions packages/agents/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const entries = [
"src/mcp/do-oauth-client-provider.ts",
"src/mcp/x402.ts",
"src/observability/index.ts",
"src/schedules/index.ts",
"src/codemode/ai.ts",
"src/experimental/memory/session/index.ts",
"src/experimental/memory/utils/index.ts",
Expand Down
89 changes: 89 additions & 0 deletions packages/agents/src/agent/__tests__/schedules-lifecycle.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, it, vi } from "vitest";
import type { Agent } from "../..";
import { registerAgentSchedulerHost } from "../../schedules/host";
import { AgentScheduler } from "../../schedules/manager";

function createSchedulerHarness(initialVersion?: number) {
const owner = {} as Agent;
let version = initialVersion;
const queries: string[] = [];
const put = vi.fn(async (_key: string, value: number) => {
version = value;
});

registerAgentSchedulerHost(owner, {
agent: owner,
storage: {
get: vi.fn(async () => version),
put
} as unknown as DurableObjectStorage,
sql: vi.fn() as never,
rawSql: ((query: string) => {
queries.push(query);
return {
toArray: () =>
query.includes("sqlite_master")
? [
{
sql: "CREATE TABLE cf_agents_schedules (type TEXT CHECK(type IN ('scheduled', 'delayed', 'cron', 'interval')))"
}
]
: []
};
}) as SqlStorage["exec"],
emit: vi.fn(),
retryDefaults: () => ({
maxAttempts: 3,
baseDelayMs: 100,
maxDelayMs: 3000
}),
hungScheduleTimeoutSeconds: () => 300,
validateScheduleCallback: vi.fn(),
isFacet: () => false,
selfPath: () => [],
rootAlarmOwner: vi.fn() as never,
isSameAgentPathPrefix: () => false,
dispatchFacetCallback: vi.fn() as never,
scheduleNextAlarm: vi.fn(),
isDestroyed: () => false,
onError: vi.fn()
});

return { owner, put, queries };
}

describe("AgentScheduler lifecycle", () => {
it("does not migrate while default and replacement components construct", () => {
const { owner, queries } = createSchedulerHarness();

new AgentScheduler(owner);
new AgentScheduler(owner);

expect(queries).toEqual([]);
});

it("migrates the installed component once on start", async () => {
const { owner, put, queries } = createSchedulerHarness();
const scheduler = new AgentScheduler(owner);

await scheduler.onStart({ props: undefined });
const queryCount = queries.length;

expect(queryCount).toBeGreaterThan(0);
expect(put).toHaveBeenCalledWith("cf_agents:schedules_schema_version", 1);

await scheduler.onStart({ props: undefined });
expect(queries).toHaveLength(queryCount);
expect(put).toHaveBeenCalledTimes(1);
});

it("skips migration when the component schema is current", async () => {
const { owner, put, queries } = createSchedulerHarness(1);
const scheduler = new AgentScheduler(owner);

await scheduler.onStart({ props: undefined });

expect(queries).toEqual([]);
expect(put).not.toHaveBeenCalled();
});
});
Loading
Loading