Skip to content

Commit 481950c

Browse files
sergicalJPeer264
authored andcommitted
fix(v10/cloudflare): Set agent conversation id on the onRequest path
Backport of: #22846
1 parent e00b1ae commit 481950c

5 files changed

Lines changed: 75 additions & 3 deletions

File tree

dev-packages/e2e-tests/test-applications/cloudflare-agent/tests/ai-streaming.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ function assertGenAiStreamingSpan(spans: Array<Record<string, any>> | undefined)
2525
'gen_ai.usage.input_tokens': 15,
2626
'gen_ai.usage.output_tokens': 8,
2727
'gen_ai.usage.total_tokens': 23,
28+
// Both agents are addressed as `.../test`, so the instance name is the conversation id.
29+
'gen_ai.conversation.id': 'test',
2830
}),
2931
);
3032
}

packages/cloudflare/src/instrumentations/agents/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { instrumentAgentCallableRpc } from './instrumentAgentCallableRpc';
2+
import { instrumentAgentRequestConversation } from './instrumentAgentRequestConversation';
23
import { instrumentChatAgentConversation } from './instrumentChatAgentConversation';
34
import type { AgentInternals } from './types';
45

@@ -8,9 +9,9 @@ import type { AgentInternals } from './types';
89
*
910
* - **Callable RPC spans** — a span (op `rpc`) for each `@callable()` method invoked over WebSocket.
1011
* - **Conversation correlation** — sets the conversation id on the scope for each unit of agent
11-
* work — chat turn or callable RPC call — so `gen_ai` spans created within it are correlated, for
12-
* chat and plain agents alike. Defaults to the instance `name` and is rotated when the chat is
13-
* cleared (the `message:clear` observability event).
12+
* work — chat turn, callable RPC call, or HTTP request — so `gen_ai` spans created within it are
13+
* correlated, for chat and plain agents alike. Defaults to the instance `name` and is rotated
14+
* when the chat is cleared (the `message:clear` observability event).
1415
*
1516
* It only hooks the `agents` package internals and uses Sentry's tracing primitives. On Cloudflare
1617
* Workers, prefer `instrumentAgentWithSentry`, which additionally instruments the Durable Object
@@ -29,6 +30,7 @@ export function instrumentCloudflareAgent<T extends object>(agent: T): T {
2930

3031
instrumentAgentCallableRpc(internals);
3132
instrumentChatAgentConversation(internals);
33+
instrumentAgentRequestConversation(internals);
3234

3335
return agent;
3436
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { type AgentInternals, setAgentConversationId } from './types';
2+
3+
/**
4+
* Correlates the AI spans of an HTTP-driven agent turn with a conversation id on the active scope.
5+
*
6+
* `onRequest` is the third unit of agent work, alongside chat turns and `@callable()` RPC: the
7+
* `agents` router sends every non-WebSocket request to it, which is how REST endpoints and webhooks
8+
* reach an agent.
9+
*
10+
* `agents` installs `onRequest` as an own property in the `Agent` constructor (as it does
11+
* `onMessage`), and we instrument after construction, so wrapping the own property is what the
12+
* router ends up calling.
13+
*/
14+
export function instrumentAgentRequestConversation(obj: AgentInternals): void {
15+
const original = obj.onRequest;
16+
17+
if (typeof original !== 'function') {
18+
return;
19+
}
20+
21+
obj.onRequest = new Proxy(original, {
22+
apply(target, thisArg: AgentInternals, args: unknown[]): unknown {
23+
setAgentConversationId(thisArg);
24+
25+
return Reflect.apply(target, thisArg, args);
26+
},
27+
});
28+
}

packages/cloudflare/src/instrumentations/agents/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface AgentInternals {
1919
* does not, so its presence discriminates a chat agent.
2020
*/
2121
onChatMessage?: (...args: unknown[]) => unknown;
22+
/** HTTP request handler; the router sends every non-WebSocket request here. */
23+
onRequest?: (...args: unknown[]) => unknown;
2224
/** The user's Agent class (used by the SDK for the observability event `agent` field). */
2325
_ParentClass?: { name?: string };
2426
/** The Agent instance name, which in the Agents model identifies the conversation/thread. */

packages/cloudflare/test/instrumentCloudflareAgent.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,43 @@ describe('instrumentCloudflareAgent', () => {
157157
expect(agent.seenConversationId).toBe('instance-1');
158158
expect('onChatMessage' in agent).toBe(false);
159159
});
160+
161+
it('sets the conversation id during an HTTP request', () => {
162+
const agent = createFakeAgent({
163+
onRequest(this: any) {
164+
this.seenConversationId = getCurrentScope().getScopeData().conversationId;
165+
return 'response';
166+
},
167+
});
168+
instrumentCloudflareAgent(agent);
169+
170+
const result = agent.onRequest(new Request('https://example.com/agents/my-agent/instance-1'));
171+
172+
expect(result).toBe('response');
173+
expect(agent.seenConversationId).toBe('instance-1');
174+
});
175+
176+
it('prefers the rotated conversation id over the instance name on the HTTP path', () => {
177+
const agent = createFakeAgent({
178+
onRequest(this: any) {
179+
this.seenConversationId = getCurrentScope().getScopeData().conversationId;
180+
return 'response';
181+
},
182+
});
183+
instrumentCloudflareAgent(agent);
184+
185+
agent._emit?.('message:clear');
186+
agent.__sentryConversationId = 'rotated-id';
187+
agent.onRequest(new Request('https://example.com/agents/my-agent/instance-1'));
188+
189+
expect(agent.seenConversationId).toBe('rotated-id');
190+
});
191+
192+
it('does not throw when the agent has no onRequest handler', () => {
193+
const agent = createFakeAgent();
194+
195+
expect(() => instrumentCloudflareAgent(agent)).not.toThrow();
196+
expect('onRequest' in agent).toBe(false);
197+
});
160198
});
161199
});

0 commit comments

Comments
 (0)