Skip to content

Allow callable to be used as a function.#1698

Open
cpojer wants to merge 1 commit into
cloudflare:mainfrom
cpojer:callable-fn
Open

Allow callable to be used as a function.#1698
cpojer wants to merge 1 commit into
cloudflare:mainfrom
cpojer:callable-fn

Conversation

@cpojer

@cpojer cpojer commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Decorators are problematic. They have been stuck in standardization limbo for 12 years, there are a lot of versions, and they have recently been bumped back on the path to standardization. Vite 8 does not currently support decorators, which means the agents package has to add Babel to pre-process JS which slows down compilation quite a bit. With agents, the value of decorators as syntax sugar may not be so clear anymore.

This PR changes callable into a hybrid decorator and function so you can instead define callback functions like this:

import { Agent, callable } from "agents";

export class GreetingAgent extends Agent {
  greet = callable(async (name: string) => {
    return `Hello, ${name}!`;
  });
}

If you are on board with merging this, I would like to follow it up with an option for the Vite plugin to skip the decorator plugin for faster compilation, and in the future we could choose to flip the default to disable decorators (+ codemod) and possibly even remove it.

Add a decorator-free way to expose callable Agent methods by allowing `callable(fn, metadata)` on function-valued class fields while keeping the existing `@callable()` decorator path intact. Both forms share the same callable metadata, and the runtime now normalizes own callable fields before RPC dispatch and method introspection so they retain Agent context and appear in `getCallableMethods()`.
@cpojer cpojer requested a review from threepointone June 8, 2026 06:56
@changeset-bot

changeset-bot Bot commented Jun 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b9a4576

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
agents Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1698

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1698

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1698

create-think

npm i https://pkg.pr.new/create-think@1698

hono-agents

npm i https://pkg.pr.new/hono-agents@1698

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1698

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1698

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1698

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1698

commit: b9a4576

@Boshen Boshen left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Context: ECMAScript decorators were moved from Stage 3 to Stage 2.7 in tc39/proposals@cd2396f.

At present, I don’t recommend using them, since we only use this syntax in a single place. The lack of tooling support and the confusing tsconfig setup mentioned in the docs are what prompted this PR.

I have a few ideas for making ECMAScript decorators generally available in the Vite ecosystem, but that will take a while because it depends on the standard TC39 process.

@threepointone

Copy link
Copy Markdown
Contributor

I love that this has backward compat. I need to check some stuff with making sure regular rpc works with this and I'll land it (it might not, I think that's the problem with instance props and why we went with decorators here)

@ingrave-solucoes

Copy link
Copy Markdown

Code review

Found 3 critical issues:

  1. Metadata storage inconsistency between decorator and function forms (packages/agents/src/index.ts:129-131)

The current implementation uses callableMetadata.set(target, metadata) which relies on function reference identity. When using callable(async (name: string) => {...}), the wrapped function has a different reference than the original, which could break metadata lookup in _isCallable().

message:
| EmailMessage
| {

  1. Type safety compromise in hybrid approach (packages/agents/src/index.ts:123-135)

The hybrid approach requires complex conditional types that could compromise type safety. The current implementation only supports the decorator pattern cleanly, and adding function wrapping creates type complexity that may lead to runtime errors.

/**
* Structural type for Cloudflare's `send_email` binding.
* Accepts both raw MIME messages and structured builder objects.
*/
export type EmailSendBinding = {
send(
message:
| EmailMessage
| {
from: string | { email: string; name?: string };
to: string | string[];
subject: string;
replyTo?: string | { email: string; name?: string };

  1. Backward compatibility risks with auto-wrapping (packages/agents/src/index.ts:847-906)

The _autoWrapCustomMethods() function automatically wraps methods with agent context. This could interfere with the new function form and potentially break existing functionality that relies on the current decorator behavior.

type AgentMcpOAuthProvider,
/** @deprecated Use {@link AgentMcpOAuthProvider} instead. */
type AgentsOAuthProvider
} from "./mcp/do-oauth-client-provider";
/**
* MCP Server state update message from server -> Client
*/
export type MCPServerMessage = {
type: MessageType.CF_AGENT_MCP_SERVERS;
mcp: MCPServersState;
};
export type MCPServersState = {
servers: {
[id: string]: MCPServer;
};
tools: (Tool & { serverId: string })[];
prompts: (Prompt & { serverId: string })[];
resources: (Resource & { serverId: string })[];
};
export type MCPServer = {
name: string;
server_url: string;
auth_url: string | null;
// This state is specifically about the temporary process of getting a token (if needed).
// Scope outside of that can't be relied upon because when the DO sleeps, there's no way
// to communicate a change to a non-ready state.
state: MCPConnectionState;
/** May contain untrusted content from external OAuth providers. Escape appropriately for your output context. */
error: string | null;
instructions: string | null;
capabilities: ServerCapabilities | null;
};
/**
* Options for adding an MCP server
*/
export type AddMcpServerOptions = {
/**
* Optional caller-supplied stable server id. When provided, this id is used
* for storage, restore, and tool-name namespacing instead of a generated
* `nanoid`. The value is normalized via {@link normalizeServerId} — for
* connector-style integrations this lets `addMcpServer` keep producing
* keys like `tool_github_create_pull_request`.
*
* Throws if an existing server already uses the same (normalized) id but a
* different name or url.
*/
id?: string;
/** OAuth callback host (auto-derived from request if omitted) */
callbackHost?: string;
/**
* Custom callback URL path — bypasses the default `/agents/{class}/{name}/callback` construction.
* Required when `sendIdentityOnConnect` is `false` to prevent leaking the instance name.
* When set, the callback URL becomes `{callbackHost}/{callbackPath}`.
* The developer must route this path to the agent instance via `getAgentByName`.
* Should be a plain path (e.g., `/mcp-callback`) — do not include query strings or fragments.
*/

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants