@@ -4,10 +4,45 @@ import { Subject } from 'rxjs';
44import type { AbstractAgent } from '@ag-ui/client' ;
55import type {
66 Agent , Message , AgentStatus , ToolCall , AgentEvent ,
7+ AgentRuntimeTelemetryEvent ,
8+ AgentRuntimeTelemetryProperties ,
9+ AgentRuntimeTelemetrySink ,
710 AgentSubmitInput , AgentSubmitOptions ,
811} from '@ngaf/chat' ;
912import { reduceEvent , type ReducerStore } from './reducer' ;
1013
14+ export interface ToAgentOptions {
15+ /** Optional app-owned telemetry sink. No telemetry is emitted unless this is provided. */
16+ telemetry ?: AgentRuntimeTelemetrySink | false ;
17+ }
18+
19+ function captureAgentRuntimeTelemetry (
20+ sink : AgentRuntimeTelemetrySink | false | undefined ,
21+ event : AgentRuntimeTelemetryEvent ,
22+ properties : AgentRuntimeTelemetryProperties ,
23+ ) : void {
24+ if ( ! sink ) return ;
25+ try {
26+ void Promise . resolve ( sink ( { event, properties } ) ) . catch ( ( ) => undefined ) ;
27+ } catch {
28+ // Keep telemetry side effects isolated from adapter control flow.
29+ }
30+ }
31+
32+ function agentRuntimeTelemetryErrorClass ( error : unknown ) : string {
33+ if ( error instanceof Error ) return error . name || error . constructor . name || 'Error' ;
34+ if (
35+ error
36+ && typeof error === 'object'
37+ && 'name' in error
38+ && typeof error . name === 'string'
39+ && error . name . length > 0
40+ ) {
41+ return error . name ;
42+ }
43+ return 'UnknownError' ;
44+ }
45+
1146/**
1247 * Wraps an AG-UI AbstractAgent into the runtime-neutral Agent contract.
1348 *
@@ -22,7 +57,7 @@ import { reduceEvent, type ReducerStore } from './reducer';
2257 * agent instance they constructed. The subscriber registered via
2358 * source.subscribe() will fire for the lifetime of source.
2459 */
25- export function toAgent ( source : AbstractAgent ) : Agent {
60+ export function toAgent ( source : AbstractAgent , options : ToAgentOptions = { } ) : Agent {
2661 const store : ReducerStore = {
2762 messages : signal < Message [ ] > ( [ ] ) ,
2863 status : signal < AgentStatus > ( 'idle' ) ,
@@ -32,6 +67,45 @@ export function toAgent(source: AbstractAgent): Agent {
3267 state : signal < Record < string , unknown > > ( { } ) ,
3368 events$ : new Subject < AgentEvent > ( ) ,
3469 } ;
70+ const telemetryProperties = { transport : 'ag-ui' as const , surface : 'to_agent' } ;
71+ let activeRun : { startedAt : number ; errored : boolean } | null = null ;
72+
73+ captureAgentRuntimeTelemetry (
74+ options . telemetry ,
75+ 'ngaf:runtime_instance_created' ,
76+ telemetryProperties ,
77+ ) ;
78+
79+ function startRunTelemetry ( requestType : string ) : { startedAt : number ; errored : boolean } {
80+ const run = { startedAt : Date . now ( ) , errored : false } ;
81+ activeRun = run ;
82+ captureAgentRuntimeTelemetry ( options . telemetry , 'ngaf:runtime_request_created' , {
83+ ...telemetryProperties ,
84+ requestType,
85+ } ) ;
86+ captureAgentRuntimeTelemetry ( options . telemetry , 'ngaf:stream_started' , telemetryProperties ) ;
87+ return run ;
88+ }
89+
90+ function finishRunTelemetry ( run : { startedAt : number ; errored : boolean } ) : void {
91+ if ( run . errored ) return ;
92+ captureAgentRuntimeTelemetry ( options . telemetry , 'ngaf:stream_ended' , {
93+ ...telemetryProperties ,
94+ durationMs : Date . now ( ) - run . startedAt ,
95+ } ) ;
96+ if ( activeRun === run ) activeRun = null ;
97+ }
98+
99+ function failRunTelemetry ( error : unknown , run = activeRun ) : void {
100+ if ( ! run || run . errored ) return ;
101+ run . errored = true ;
102+ captureAgentRuntimeTelemetry ( options . telemetry , 'ngaf:stream_errored' , {
103+ ...telemetryProperties ,
104+ durationMs : Date . now ( ) - run . startedAt ,
105+ errorClass : agentRuntimeTelemetryErrorClass ( error ) ,
106+ } ) ;
107+ if ( activeRun === run ) activeRun = null ;
108+ }
35109
36110 // Tap all events from the source agent via the AgentSubscriber API.
37111 // This subscription lives for the lifetime of `source`.
@@ -43,6 +117,7 @@ export function toAgent(source: AbstractAgent): Agent {
43117 store . status . set ( 'error' ) ;
44118 store . isLoading . set ( false ) ;
45119 store . error . set ( error ) ;
120+ failRunTelemetry ( error ) ;
46121 } ,
47122 } ) ;
48123
@@ -65,14 +140,17 @@ export function toAgent(source: AbstractAgent): Agent {
65140 source . addMessage ( userMsg as Parameters < typeof source . addMessage > [ 0 ] ) ;
66141 }
67142
143+ const run = startRunTelemetry ( 'submit' ) ;
68144 try {
69145 await source . runAgent ( ) ;
146+ finishRunTelemetry ( run ) ;
70147 } catch ( err ) {
71148 // If the run was aborted via stop(), abortRun() resolves the promise
72149 // rather than rejecting — but catch any unexpected errors here.
73150 store . status . set ( 'error' ) ;
74151 store . isLoading . set ( false ) ;
75152 store . error . set ( err ) ;
153+ failRunTelemetry ( err , run ) ;
76154 }
77155 } ,
78156
@@ -113,12 +191,15 @@ export function toAgent(source: AbstractAgent): Agent {
113191 // message in `trimmed` becomes the active prompt for the next run.
114192 source . setMessages ( trimmed as Parameters < typeof source . setMessages > [ 0 ] ) ;
115193
194+ const run = startRunTelemetry ( 'regenerate' ) ;
116195 try {
117196 await source . runAgent ( ) ;
197+ finishRunTelemetry ( run ) ;
118198 } catch ( err ) {
119199 store . status . set ( 'error' ) ;
120200 store . isLoading . set ( false ) ;
121201 store . error . set ( err ) ;
202+ failRunTelemetry ( err , run ) ;
122203 }
123204 } ,
124205 } ;
0 commit comments