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
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Agent, callable } from 'agents';

/**
* An Agent declared outside the worker entry, which imports it and exports it again by specifier
* (`import { ImportedAgent } from './imported-agent'; export { ImportedAgent }`). The entry has no
* local class to rename, so the plugin has to re-point the export at a wrapper binding instead.
*/
export class ImportedAgent extends Agent<Env> {
@callable()
async greet(name: string): Promise<string> {
return `Hello, ${name}! (from ImportedAgent)`;
}

async onRequest(): Promise<Response> {
return Response.json({ agent: 'imported' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ import { AIChatAgent } from '@cloudflare/ai-chat';
import { Agent, callable, routeAgentRequest } from 'agents';
import { DurableObject } from 'cloudflare:workers';
import { MyBase } from './base';
import { ImportedAgent } from './imported-agent';

export { ImportedAgent };
export { ReExportedAgent } from './reexported-agent';

// The two exports above live in their own modules — see `imported-agent.ts` and
// `reexported-agent.ts`. They cover the shapes an entry that only aggregates
// classes uses, where there is no local declaration for the plugin to rewrite.
//
// NOTE: this file deliberately contains NO `Sentry.*` calls and no import of
// `@sentry/cloudflare`. Everything below is wrapped at build time by
// `sentryCloudflareVitePlugin({ _experimental: { autoInstrumentation: true } })`,
// which reads wrangler.jsonc, wraps the default export with `withSentry`, and
// picks a wrapper per class: `instrumentAgentWithSentry` for the three Agents,
// picks a wrapper per class: `instrumentAgentWithSentry` for the five Agents,
// `instrumentDurableObjectWithSentry` for the plain Durable Object.
//
// Options come from `instrument.server.ts` next to this entry.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Agent, callable } from 'agents';

/**
* An Agent the worker entry only ever re-exports (`export { ReExportedAgent } from
* './reexported-agent'`) — it never binds the class locally at all, so the plugin has to import it
* under a private name before it can wrap it.
*/
export class ReExportedAgent extends Agent<Env> {
@callable()
async greet(name: string): Promise<string> {
return `Hello, ${name}! (from ReExportedAgent)`;
}

async onRequest(): Promise<Response> {
return Response.json({ agent: 'reexported' });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ for (const { title, binding, agentClass } of [
binding: 'derived-agent',
agentClass: 'DerivedAgent',
},
{
title: 'an Agent imported from another module and exported by specifier',
binding: 'imported-agent',
agentClass: 'ImportedAgent',
},
{
title: 'an Agent re-exported straight from another module',
binding: 're-exported-agent',
agentClass: 'ReExportedAgent',
},
]) {
test(`applies agent instrumentation to ${title}`, async ({ baseURL }) => {
const instance = `${binding}-instance`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,19 @@
// "this one is an Agent". Only the base-class chain distinguishes them, which
// is exactly what the plugin's detection has to work out at build time:
//
// MyAgent -> extends Agent (entry-local) => agent
// MyChatAgent -> extends AIChatAgent (entry-local) => agent
// DerivedAgent -> extends ./base#MyBase -> Agent => agent
// PlainDO -> extends DurableObject => durableObject
// MyAgent -> extends Agent (entry-local) => agent
// MyChatAgent -> extends AIChatAgent (entry-local) => agent
// DerivedAgent -> extends ./base#MyBase -> Agent => agent
// ImportedAgent -> imported, then `export { ImportedAgent }` => agent
// ReExportedAgent -> `export { ... } from './reexported-agent'` => agent
// PlainDO -> extends DurableObject => durableObject
"durable_objects": {
"bindings": [
{ "name": "MyAgent", "class_name": "MyAgent" },
{ "name": "MyChatAgent", "class_name": "MyChatAgent" },
{ "name": "DerivedAgent", "class_name": "DerivedAgent" },
{ "name": "ImportedAgent", "class_name": "ImportedAgent" },
{ "name": "ReExportedAgent", "class_name": "ReExportedAgent" },
{ "name": "PlainDO", "class_name": "PlainDO" },
],
},
Expand All @@ -28,5 +32,9 @@
"tag": "v1",
"new_sqlite_classes": ["MyAgent", "MyChatAgent", "DerivedAgent", "PlainDO"],
},
{
"tag": "v2",
"new_sqlite_classes": ["ImportedAgent", "ReExportedAgent"],
},
],
}
14 changes: 9 additions & 5 deletions packages/cloudflare/src/vite/agentClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,21 +80,25 @@ export async function detectAgentClasses(
}

/**
* The local class names in the entry that a configured class name could refer to — either declared
* under that name directly, or aliased to it by an `export { Local as Configured }` specifier.
* The entry-module binding names a configured class name could refer to — a class declared under
* that name, an import of it from another module, a `export { X } from './x'` re-export, or the
* local binding an `export { Local as Configured }` specifier aliases.
*
* Keeps detection (which reads and scans other modules) off classes that no binding points at.
* Keeps detection (which reads and scans other modules) off names no binding points at.
*/
export function collectAgentCandidates(ast: ProgramBody, configuredNames: Iterable<string>): Set<string> {
const shape = shapeFromAst(ast);
const candidates = new Set<string>();

const isResolvable = (name: string): boolean =>
shape.classes.has(name) || shape.imports.has(name) || shape.reexports.has(name);

for (const configured of configuredNames) {
if (shape.classes.has(configured)) {
if (isResolvable(configured)) {
candidates.add(configured);
}
const local = shape.localExports.get(configured);
if (local && shape.classes.has(local)) {
if (local && isResolvable(local)) {
candidates.add(local);
}
}
Expand Down
7 changes: 4 additions & 3 deletions packages/cloudflare/src/vite/autoInstrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,10 @@ export function sentryCloudflareAutoInstrumentPlugin(options: { wranglerConfigPa
const missing = [...classWrappers.keys()].filter(name => !wrappedClasses.has(name));
if (missing.length > 0) {
this.warn?.(
`[sentry] Could not auto-instrument class(es) ${missing.join(', ')}: no matching exported class ` +
'declaration found in the worker entry (re-exports from other modules cannot be wrapped ' +
'automatically). Wrap them manually with the matching `instrument*WithSentry` helper.',
`[sentry] Could not auto-instrument class(es) ${missing.join(', ')}: the worker entry has no ` +
'export naming them (a star re-export like `export * from "./do"` does not name its ' +
'exports). Export them by name, or wrap them manually with the matching ' +
'`instrument*WithSentry` helper.',
);
}

Expand Down
Loading
Loading