@microsoft/agents-hosting 1.5.0-beta.10.gc9dfbe84d2 → 1.5.0-beta.12.ga9a2b23c19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/package.json +2 -2
  2. package/dist/src/app/agentApplication.d.ts +14 -0
  3. package/dist/src/app/agentApplication.js +31 -2
  4. package/dist/src/app/agentApplication.js.map +1 -1
  5. package/dist/src/app/agentApplicationBuilder.d.ts +7 -0
  6. package/dist/src/app/agentApplicationBuilder.js +9 -0
  7. package/dist/src/app/agentApplicationBuilder.js.map +1 -1
  8. package/dist/src/app/agentApplicationOptions.d.ts +6 -0
  9. package/dist/src/app/index.d.ts +1 -0
  10. package/dist/src/app/index.js +1 -0
  11. package/dist/src/app/index.js.map +1 -1
  12. package/dist/src/app/proactive/conversation.d.ts +43 -0
  13. package/dist/src/app/proactive/conversation.js +67 -0
  14. package/dist/src/app/proactive/conversation.js.map +1 -0
  15. package/dist/src/app/proactive/conversationBuilder.d.ts +54 -0
  16. package/dist/src/app/proactive/conversationBuilder.js +110 -0
  17. package/dist/src/app/proactive/conversationBuilder.js.map +1 -0
  18. package/dist/src/app/proactive/conversationReferenceBuilder.d.ts +68 -0
  19. package/dist/src/app/proactive/conversationReferenceBuilder.js +125 -0
  20. package/dist/src/app/proactive/conversationReferenceBuilder.js.map +1 -0
  21. package/dist/src/app/proactive/createConversationOptions.d.ts +30 -0
  22. package/dist/src/app/proactive/createConversationOptions.js +10 -0
  23. package/dist/src/app/proactive/createConversationOptions.js.map +1 -0
  24. package/dist/src/app/proactive/createConversationOptionsBuilder.d.ts +69 -0
  25. package/dist/src/app/proactive/createConversationOptionsBuilder.js +141 -0
  26. package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -0
  27. package/dist/src/app/proactive/index.d.ts +7 -0
  28. package/dist/src/app/proactive/index.js +26 -0
  29. package/dist/src/app/proactive/index.js.map +1 -0
  30. package/dist/src/app/proactive/proactive.d.ts +248 -0
  31. package/dist/src/app/proactive/proactive.js +271 -0
  32. package/dist/src/app/proactive/proactive.js.map +1 -0
  33. package/dist/src/app/proactive/proactiveOptions.d.ts +19 -0
  34. package/dist/src/app/proactive/proactiveOptions.js +5 -0
  35. package/dist/src/app/proactive/proactiveOptions.js.map +1 -0
  36. package/dist/src/errorHelper.js +94 -0
  37. package/dist/src/errorHelper.js.map +1 -1
  38. package/package.json +2 -2
  39. package/src/app/agentApplication.ts +33 -0
  40. package/src/app/agentApplicationBuilder.ts +11 -0
  41. package/src/app/agentApplicationOptions.ts +7 -0
  42. package/src/app/index.ts +1 -0
  43. package/src/app/proactive/conversation.ts +87 -0
  44. package/src/app/proactive/conversationBuilder.ts +139 -0
  45. package/src/app/proactive/conversationReferenceBuilder.ts +161 -0
  46. package/src/app/proactive/createConversationOptions.ts +35 -0
  47. package/src/app/proactive/createConversationOptionsBuilder.ts +181 -0
  48. package/src/app/proactive/index.ts +10 -0
  49. package/src/app/proactive/proactive.ts +481 -0
  50. package/src/app/proactive/proactiveOptions.ts +24 -0
  51. package/src/errorHelper.ts +108 -0
@@ -0,0 +1,181 @@
1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
4
+ import type { Activity, ChannelAccount, ConversationParameters } from '@microsoft/agents-activity'
5
+ import { Channels, ExceptionHelper, RoleTypes } from '@microsoft/agents-activity'
6
+ import { Errors } from '../../errorHelper'
7
+ import { AzureBotScope, type CreateConversationOptions } from './createConversationOptions'
8
+ import type { ConversationClaims } from './conversation'
9
+ import { ConversationReferenceBuilder } from './conversationReferenceBuilder'
10
+
11
+ /**
12
+ * Fluent builder for `CreateConversationOptions`.
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const opts = CreateConversationOptionsBuilder
17
+ * .create('my-client-id', 'msteams')
18
+ * .withUser('user-aad-id')
19
+ * .withTenantId('tenant-id')
20
+ * .build()
21
+ * ```
22
+ */
23
+ export class CreateConversationOptionsBuilder {
24
+ private readonly _claims: ConversationClaims
25
+ private readonly _channelId: string
26
+ private readonly _serviceUrl: string
27
+ private _scope: string = AzureBotScope
28
+ private _storeConversation: boolean = false
29
+ private _parameters: Partial<ConversationParameters> = {
30
+ channelData: {},
31
+ }
32
+
33
+ private _activity: Partial<Activity> | undefined
34
+
35
+ private constructor (claims: ConversationClaims, channelId: string, serviceUrl?: string) {
36
+ this._claims = claims
37
+ this._channelId = channelId
38
+ this._serviceUrl =
39
+ serviceUrl ?? ConversationReferenceBuilder.serviceUrlForChannel(channelId)
40
+ }
41
+
42
+ /**
43
+ * Creates a new builder from an agent client ID string.
44
+ * @param agentClientId The agent's client (app) ID.
45
+ * @param channelId The target channel (e.g. `'msteams'`).
46
+ * @param serviceUrl Optional service URL override.
47
+ */
48
+ static create (agentClientId: string, channelId: string, serviceUrl?: string, parameters?: Partial<ConversationParameters>): CreateConversationOptionsBuilder
49
+ /**
50
+ * Creates a new builder from an existing claims object (e.g. from a stored `Conversation`).
51
+ * @param claims JWT claims — `aud` must be the agent's client ID.
52
+ * @param channelId The target channel (e.g. `'msteams'`).
53
+ * @param serviceUrl Optional service URL override.
54
+ */
55
+ static create (claims: ConversationClaims, channelId: string, serviceUrl?: string, parameters?: Partial<ConversationParameters>): CreateConversationOptionsBuilder
56
+ static create (
57
+ agentClientIdOrClaims: string | ConversationClaims,
58
+ channelId: string,
59
+ serviceUrl?: string,
60
+ parameters?: Partial<ConversationParameters>
61
+ ): CreateConversationOptionsBuilder {
62
+ const claims: ConversationClaims = typeof agentClientIdOrClaims === 'string'
63
+ ? { aud: agentClientIdOrClaims }
64
+ : agentClientIdOrClaims
65
+
66
+ const builder = new CreateConversationOptionsBuilder(claims, channelId, serviceUrl)
67
+ if (parameters) {
68
+ builder._parameters = { ...builder._parameters, ...parameters }
69
+ }
70
+
71
+ // Set parameters.agent if not already provided — matches C# behavior
72
+ if (!builder._parameters.agent) {
73
+ builder._parameters.agent = { id: claims.aud, role: RoleTypes.Agent }
74
+ }
75
+
76
+ return builder
77
+ }
78
+
79
+ /** Adds a member (the target user) to `parameters.members`. */
80
+ withUser (userId: string, userName?: string): this
81
+ withUser (account: ChannelAccount): this
82
+ withUser (userIdOrAccount: string | ChannelAccount, userName?: string): this {
83
+ const account: ChannelAccount =
84
+ typeof userIdOrAccount === 'string'
85
+ ? { id: userIdOrAccount, name: userName }
86
+ : userIdOrAccount
87
+ const members = this._parameters.members ?? []
88
+ members.push(account)
89
+ this._parameters = { ...this._parameters, members }
90
+ return this
91
+ }
92
+
93
+ /** Sets `parameters.activity`. Defaults `activity.type` to `'message'` if not provided. */
94
+ withActivity (activity: Partial<Activity>): this {
95
+ this._activity = activity
96
+ return this
97
+ }
98
+
99
+ /** Merges additional channel-specific data into `parameters.channelData`. */
100
+ withChannelData (data: object): this {
101
+ this._parameters = {
102
+ ...this._parameters,
103
+ channelData: { ...(this._parameters.channelData as object ?? {}), ...data },
104
+ }
105
+ return this
106
+ }
107
+
108
+ /**
109
+ * Sets `parameters.tenantId`.
110
+ * On `msteams` channels, also sets `channelData.tenant.id`.
111
+ */
112
+ withTenantId (tenantId: string): this {
113
+ this._parameters = { ...this._parameters, tenantId }
114
+ if (this._channelId === Channels.Msteams) {
115
+ this.withChannelData({ tenant: { id: tenantId } })
116
+ }
117
+ return this
118
+ }
119
+
120
+ /**
121
+ * Sets `parameters.isGroup = true` and `channelData.channel.id`.
122
+ * Only has effect on `msteams` channels.
123
+ */
124
+ withTeamsChannelId (teamsChannelId: string): this {
125
+ if (this._channelId !== Channels.Msteams) return this
126
+ this._parameters = { ...this._parameters, isGroup: true }
127
+ this.withChannelData({ channel: { id: teamsChannelId } })
128
+ return this
129
+ }
130
+
131
+ /** Sets `parameters.topicName`. */
132
+ withTopicName (name: string): this {
133
+ this._parameters = { ...this._parameters, topicName: name }
134
+ return this
135
+ }
136
+
137
+ /** Sets `parameters.isGroup`. */
138
+ isGroup (value: boolean): this {
139
+ this._parameters = { ...this._parameters, isGroup: value }
140
+ return this
141
+ }
142
+
143
+ /** Overrides the default `AzureBotScope` OAuth scope. */
144
+ withScope (scope: string): this {
145
+ this._scope = scope
146
+ return this
147
+ }
148
+
149
+ /** Controls whether the resulting conversation is stored after creation. */
150
+ storeConversation (value: boolean): this {
151
+ this._storeConversation = value
152
+ return this
153
+ }
154
+
155
+ /**
156
+ * Builds and returns `CreateConversationOptions`.
157
+ * @throws if no members were added via `withUser()`.
158
+ */
159
+ build (): CreateConversationOptions {
160
+ if (!this._parameters.members?.length) {
161
+ throw ExceptionHelper.generateException(Error, Errors.CreateConversationBuilderMembersRequired)
162
+ }
163
+
164
+ const activity: Partial<Activity> = {
165
+ type: 'message',
166
+ ...this._activity,
167
+ }
168
+
169
+ return {
170
+ identity: this._claims,
171
+ channelId: this._channelId,
172
+ serviceUrl: this._serviceUrl,
173
+ scope: this._scope,
174
+ storeConversation: this._storeConversation,
175
+ parameters: {
176
+ ...this._parameters,
177
+ activity: activity as Activity,
178
+ },
179
+ }
180
+ }
181
+ }
@@ -0,0 +1,10 @@
1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
4
+ export * from './conversation'
5
+ export * from './conversationBuilder'
6
+ export * from './conversationReferenceBuilder'
7
+ export * from './createConversationOptions'
8
+ export * from './createConversationOptionsBuilder'
9
+ export * from './proactiveOptions'
10
+ export * from './proactive'
@@ -0,0 +1,481 @@
1
+ // Copyright (c) Microsoft Corporation. All rights reserved.
2
+ // Licensed under the MIT License.
3
+
4
+ import type { Activity } from '@microsoft/agents-activity'
5
+ import type { ResourceResponse } from '../../connector-client'
6
+ import type { BaseAdapter } from '../../baseAdapter'
7
+ import type { TurnContext } from '../../turnContext'
8
+ import type { TurnState } from '../turnState'
9
+ import type { RouteHandler } from '../routeHandler'
10
+ import type { Storage } from '../../storage/storage'
11
+ import type { AgentApplication } from '../agentApplication'
12
+ import type { ProactiveOptions } from './proactiveOptions'
13
+ import type { CreateConversationOptions } from './createConversationOptions'
14
+ import { ExceptionHelper } from '@microsoft/agents-activity'
15
+ import { Conversation } from './conversation'
16
+ import { debug } from '@microsoft/agents-activity/logger'
17
+ import { Errors } from '../../errorHelper'
18
+
19
+ const logger = debug('agents:proactive')
20
+ const STORAGE_KEY_PREFIX = 'proactive/conversations/'
21
+
22
+ /**
23
+ * Provides methods for storing, retrieving, and managing conversation references to enable
24
+ * proactive messaging scenarios. Supports sending activities and continuing conversations outside
25
+ * the standard request/response flow using stored conversation references.
26
+ *
27
+ * @remarks
28
+ * Use the `Proactive` class to implement scenarios where an agent needs to initiate conversations
29
+ * or send messages to users without an incoming activity, such as notifications or scheduled alerts.
30
+ * Some operations require that conversation references be stored using {@link storeConversation}
31
+ * before they can be used.
32
+ *
33
+ * Access via `app.proactive` after configuring `proactive` in {@link AgentApplicationOptions}.
34
+ */
35
+ export class Proactive<TState extends TurnState> {
36
+ /**
37
+ * `activity.valueType` that indicates additional key/values for the ContinueConversation event.
38
+ */
39
+ static readonly ContinueConversationValueType = 'application/vnd.microsoft.activity.continueconversation+json'
40
+
41
+ private readonly _app: AgentApplication<TState>
42
+ private readonly _options: ProactiveOptions
43
+ private readonly _storage?: Storage
44
+
45
+ constructor (app: AgentApplication<TState>, options: ProactiveOptions) {
46
+ this._app = app
47
+ this._options = options
48
+ this._storage = options.storage
49
+ }
50
+
51
+ private requireStorage (): Storage {
52
+ if (!this._storage) {
53
+ throw ExceptionHelper.generateException(Error, Errors.ProactiveStorageRequired)
54
+ }
55
+ return this._storage
56
+ }
57
+
58
+ private requireAppStorage (): Storage {
59
+ const storage = this._app.options.storage
60
+ if (!storage) {
61
+ throw ExceptionHelper.generateException(Error, Errors.ProactiveAppStorageRequired)
62
+ }
63
+ return storage
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Conversation reference storage
68
+ // ---------------------------------------------------------------------------
69
+
70
+ /**
71
+ * Stores the current conversation reference from a live {@link TurnContext} in proactive storage.
72
+ *
73
+ * @param context - The context object for the current turn, containing activity and conversation
74
+ * information.
75
+ * @returns The conversation ID that can be used to retrieve the reference in future operations.
76
+ * @example
77
+ * ```typescript
78
+ * // Inside an onMessage handler — save the conversation so we can message later
79
+ * app.onActivity('message', async (ctx, state) => {
80
+ * const convId = await app.proactive.storeConversation(ctx)
81
+ * await ctx.sendActivity(`Conversation stored. ID: ${convId}`)
82
+ * })
83
+ * ```
84
+ */
85
+ storeConversation (context: TurnContext): Promise<string>
86
+ /**
87
+ * Stores an explicit {@link Conversation} object in proactive storage.
88
+ *
89
+ * @param conversation - The conversation reference record to store.
90
+ * @returns The conversation ID that can be used to retrieve the reference in future operations.
91
+ * @throws If the conversation fails validation (missing `conversation.id`, `serviceUrl`, or
92
+ * `claims.aud`).
93
+ * @example
94
+ * ```typescript
95
+ * // Build a Conversation manually and store it
96
+ * const conv = ConversationBuilder
97
+ * .create('my-app-id', 'msteams')
98
+ * .withUser('user-aad-id')
99
+ * .withConversationId('19:existing-thread-id@thread.tacv2')
100
+ * .build()
101
+ * const convId = await app.proactive.storeConversation(conv)
102
+ * ```
103
+ */
104
+ storeConversation (conversation: Conversation): Promise<string>
105
+ async storeConversation (contextOrConversation: TurnContext | Conversation): Promise<string> {
106
+ const conv =
107
+ contextOrConversation instanceof Conversation
108
+ ? contextOrConversation
109
+ : new Conversation(contextOrConversation as TurnContext)
110
+
111
+ conv.validate()
112
+ const id = conv.reference.conversation.id
113
+ await this.requireStorage().write({ [`${STORAGE_KEY_PREFIX}${id}`]: { reference: conv.reference, claims: conv.claims } })
114
+ return id
115
+ }
116
+
117
+ /**
118
+ * Retrieves the stored {@link Conversation} associated with the given conversation ID.
119
+ *
120
+ * @param conversationId - The unique identifier of the conversation to retrieve.
121
+ * @returns The stored `Conversation`, or `undefined` if no record exists for that ID.
122
+ * @example
123
+ * ```typescript
124
+ * const conv = await app.proactive.getConversation(convId)
125
+ * if (conv) {
126
+ * await app.proactive.sendActivity(adapter, conv, { text: 'Hello!' })
127
+ * }
128
+ * ```
129
+ */
130
+ async getConversation (conversationId: string): Promise<Conversation | undefined> {
131
+ const result = await this.requireStorage().read([`${STORAGE_KEY_PREFIX}${conversationId}`])
132
+ const stored = result[`${STORAGE_KEY_PREFIX}${conversationId}`] as { reference: any; claims: any } | undefined
133
+ if (!stored) return undefined
134
+ return new Conversation(stored.claims, stored.reference)
135
+ }
136
+
137
+ /**
138
+ * Retrieves the stored {@link Conversation} for the given ID, throwing if no record is found.
139
+ *
140
+ * @param conversationId - The unique identifier of the conversation to retrieve.
141
+ * @returns The stored `Conversation`.
142
+ * @throws `Error` if no conversation reference is found for the specified ID.
143
+ * @example
144
+ * ```typescript
145
+ * // Use when absence of the conversation should be treated as an error
146
+ * const conv = await app.proactive.getConversationOrThrow(convId)
147
+ * await app.proactive.sendActivity(adapter, conv, { text: 'Alert: your report is ready.' })
148
+ * ```
149
+ */
150
+ async getConversationOrThrow (conversationId: string): Promise<Conversation> {
151
+ const conv = await this.getConversation(conversationId)
152
+ if (!conv) {
153
+ throw ExceptionHelper.generateException(Error, Errors.ProactiveConversationNotFound, undefined, { conversationId })
154
+ }
155
+ return conv
156
+ }
157
+
158
+ /**
159
+ * Deletes the stored conversation reference for the given conversation ID.
160
+ *
161
+ * @param conversationId - The unique identifier of the conversation whose reference should be
162
+ * deleted.
163
+ * @remarks If no record exists for the given ID, no action is taken.
164
+ * @example
165
+ * ```typescript
166
+ * // Clean up after a conversation has ended
167
+ * app.onActivity('endOfConversation', async (ctx, state) => {
168
+ * await app.proactive.deleteConversation(ctx.activity.conversation.id)
169
+ * })
170
+ * ```
171
+ */
172
+ async deleteConversation (conversationId: string): Promise<void> {
173
+ await this.requireStorage().delete([`${STORAGE_KEY_PREFIX}${conversationId}`])
174
+ }
175
+
176
+ // ---------------------------------------------------------------------------
177
+ // Send activity
178
+ // ---------------------------------------------------------------------------
179
+
180
+ /**
181
+ * Sends an activity to a stored conversation, looking it up by ID.
182
+ *
183
+ * @param adapter - The channel adapter used to send the activity.
184
+ * @param conversationId - The ID of a conversation previously stored via {@link storeConversation}.
185
+ * @param activity - The activity to send. If `type` is not set it defaults to `'message'`.
186
+ * @returns A {@link ResourceResponse} with the ID of the sent activity.
187
+ * @throws `Error` if no conversation reference is found for the specified ID.
188
+ * @example
189
+ * ```typescript
190
+ * // Send a notification using a previously stored conversation ID
191
+ * await app.proactive.sendActivity(adapter, storedConvId, { text: 'Your order has shipped!' })
192
+ * ```
193
+ */
194
+ sendActivity (adapter: BaseAdapter, conversationId: string, activity: Partial<Activity>): Promise<ResourceResponse>
195
+ /**
196
+ * Sends an activity to an existing conversation using the provided {@link Conversation} reference.
197
+ *
198
+ * @param adapter - The channel adapter used to send the activity.
199
+ * @param conversation - A `Conversation` instance created via its constructor or
200
+ * {@link ConversationBuilder}.
201
+ * @param activity - The activity to send. If `type` is not set it defaults to `'message'`.
202
+ * @returns A {@link ResourceResponse} with the ID of the sent activity.
203
+ * @example
204
+ * ```typescript
205
+ * // Build a Conversation from a stored reference and send a message
206
+ * const conv = await app.proactive.getConversationOrThrow(convId)
207
+ * const response = await app.proactive.sendActivity(adapter, conv, { text: 'Hello from the agent!' })
208
+ * console.log('Sent activity ID:', response.id)
209
+ * ```
210
+ */
211
+ sendActivity (adapter: BaseAdapter, conversation: Conversation, activity: Partial<Activity>): Promise<ResourceResponse>
212
+ async sendActivity (
213
+ adapter: BaseAdapter,
214
+ conversationOrId: Conversation | string,
215
+ activity: Partial<Activity>
216
+ ): Promise<ResourceResponse> {
217
+ const conv =
218
+ typeof conversationOrId === 'string'
219
+ ? await this.getConversationOrThrow(conversationOrId)
220
+ : conversationOrId
221
+
222
+ const activityToSend: Partial<Activity> = { type: 'message', ...activity }
223
+
224
+ logger.info('sendActivity: conversation=%s channel=%s serviceUrl=%s',
225
+ conv.reference.conversation.id, conv.reference.channelId, conv.reference.serviceUrl)
226
+
227
+ let response: ResourceResponse | undefined
228
+ let caughtError: unknown
229
+
230
+ await adapter.continueConversation(conv.identity, conv.reference, async (ctx: TurnContext) => {
231
+ try {
232
+ const result = await ctx.sendActivity(activityToSend as Activity)
233
+ response = result as ResourceResponse
234
+ } catch (err) {
235
+ caughtError = err
236
+ }
237
+ })
238
+
239
+ if (caughtError !== undefined) {
240
+ logger.warn('sendActivity: failed for conversation=%s: %s', conv.reference.conversation.id, caughtError)
241
+ throw caughtError
242
+ }
243
+ if (response === undefined) throw ExceptionHelper.generateException(Error, Errors.ProactiveSendActivityNoResponse)
244
+ logger.debug('sendActivity: sent activity id=%s', response.id)
245
+ return response
246
+ }
247
+
248
+ // ---------------------------------------------------------------------------
249
+ // Full-turn handler (loads TurnState, handles auth tokens)
250
+ // ---------------------------------------------------------------------------
251
+
252
+ /**
253
+ * Continues a stored conversation by executing the given handler within the context of that
254
+ * conversation, looking it up by ID.
255
+ *
256
+ * See the {@link Conversation} overload for full details.
257
+ *
258
+ * @param adapter - The channel adapter used to continue the conversation.
259
+ * @param conversationId - The ID of a conversation previously stored via {@link storeConversation}.
260
+ * @param handler - The route handler to execute within the continued conversation context.
261
+ * @param autoSignInHandlers - Optional list of OAuth connection names whose tokens should be
262
+ * acquired before invoking the handler.
263
+ * @param continuationActivity - Optional activity fields merged into the continuation activity,
264
+ * making them available on `ctx.activity` inside the handler (e.g. `value`, `valueType`).
265
+ * @throws `Error` if no conversation reference is found for the specified ID.
266
+ * @example
267
+ * ```typescript
268
+ * // Scheduled job: send a daily digest to all stored conversations
269
+ * for (const convId of storedIds) {
270
+ * await app.proactive.continueConversation(adapter, convId, async (ctx, state) => {
271
+ * await ctx.sendActivity('Here is your daily digest...')
272
+ * })
273
+ * }
274
+ * ```
275
+ */
276
+ continueConversation (adapter: BaseAdapter, conversationId: string, handler: RouteHandler<TState>, autoSignInHandlers?: string[], continuationActivity?: Partial<Activity>): Promise<void>
277
+ /**
278
+ * Continues an existing conversation by executing the given handler within the context of the
279
+ * provided {@link Conversation} reference. The handler receives a {@link TurnContext} and a
280
+ * freshly loaded {@link TurnState} scoped to the original conversation, enabling the agent to
281
+ * respond as if replying to an incoming activity.
282
+ *
283
+ * @remarks
284
+ * Exceptions thrown inside the handler are captured and re-thrown after the adapter callback
285
+ * completes, since the adapter would otherwise silently swallow them.
286
+ *
287
+ * If `autoSignInHandlers` are supplied and the application has user authorization configured,
288
+ * tokens are acquired before the handler is called. If not all tokens are available and
289
+ * `proactiveOptions.failOnUnsignedInConnections` is not `false`, an error is thrown.
290
+ *
291
+ * @param adapter - The channel adapter used to continue the conversation.
292
+ * @param conversation - A `Conversation` instance created via its constructor or
293
+ * {@link ConversationBuilder}.
294
+ * @param handler - The route handler to execute within the continued conversation context.
295
+ * @param autoSignInHandlers - Optional list of OAuth connection names whose tokens should be
296
+ * acquired before invoking the handler.
297
+ * @param continuationActivity - Optional activity fields merged into the continuation activity,
298
+ * making them available on `ctx.activity` inside the handler (e.g. `value`, `valueType`).
299
+ * @example
300
+ * ```typescript
301
+ * // Continue a conversation with a custom value payload
302
+ * const conv = await app.proactive.getConversationOrThrow(convId)
303
+ * await app.proactive.continueConversation(
304
+ * adapter,
305
+ * conv,
306
+ * async (ctx, state) => {
307
+ * const payload = ctx.activity.value as { alertType: string }
308
+ * await ctx.sendActivity(`Alert triggered: ${payload.alertType}`)
309
+ * },
310
+ * undefined,
311
+ * { value: { alertType: 'threshold-exceeded' }, valueType: Proactive.ContinueConversationValueType }
312
+ * )
313
+ * ```
314
+ */
315
+ continueConversation (adapter: BaseAdapter, conversation: Conversation, handler: RouteHandler<TState>, autoSignInHandlers?: string[], continuationActivity?: Partial<Activity>): Promise<void>
316
+ async continueConversation (
317
+ adapter: BaseAdapter,
318
+ conversationOrId: Conversation | string,
319
+ handler: RouteHandler<TState>,
320
+ autoSignInHandlers?: string[],
321
+ continuationActivity?: Partial<Activity>
322
+ ): Promise<void> {
323
+ const conv =
324
+ typeof conversationOrId === 'string'
325
+ ? await this.getConversationOrThrow(conversationOrId)
326
+ : conversationOrId
327
+
328
+ logger.info('continueConversation: conversation=%s channel=%s serviceUrl=%s',
329
+ conv.reference.conversation.id, conv.reference.channelId, conv.reference.serviceUrl)
330
+
331
+ let caughtError: unknown
332
+
333
+ await adapter.continueConversation(conv.identity, conv.reference, async (ctx: TurnContext) => {
334
+ try {
335
+ // Merge caller-supplied activity fields (e.g. value, valueType) into the
336
+ // continuation activity so the handler can read request-time parameters.
337
+ if (continuationActivity) {
338
+ Object.assign(ctx.activity, continuationActivity)
339
+ }
340
+
341
+ const state = this._app.options.turnStateFactory()
342
+ await state.load(ctx, this.requireAppStorage())
343
+
344
+ // Token acquisition (optional — only when auth is configured)
345
+ if (autoSignInHandlers?.length && this._app.hasUserAuthorization) {
346
+ logger.debug('continueConversation: acquiring tokens for handlers: %o', autoSignInHandlers)
347
+ const results = await Promise.all(
348
+ autoSignInHandlers.map((handlerId) =>
349
+ this._app.authorization.getToken(ctx, handlerId).catch(() => ({ token: undefined }))
350
+ )
351
+ )
352
+ const allAcquired = results.every((r) => !!r.token)
353
+ if (!allAcquired) {
354
+ logger.warn('continueConversation: not all tokens acquired for conversation=%s handlers=%o',
355
+ conv.reference.conversation.id, autoSignInHandlers)
356
+ if (this._options.failOnUnsignedInConnections !== false) {
357
+ throw ExceptionHelper.generateException(Error, Errors.ProactiveNotAllTokensAcquired)
358
+ }
359
+ }
360
+ }
361
+
362
+ await handler(ctx, state)
363
+ await state.save(ctx, this.requireAppStorage())
364
+ } catch (err) {
365
+ caughtError = err
366
+ } finally {
367
+ if ((ctx as any).streamingResponse?.isStreamStarted?.()) {
368
+ await (ctx as any).streamingResponse.endStream()
369
+ }
370
+ }
371
+ })
372
+
373
+ if (caughtError !== undefined) {
374
+ logger.warn('continueConversation: failed for conversation=%s: %s', conv.reference.conversation.id, caughtError)
375
+ throw caughtError
376
+ }
377
+ logger.debug('continueConversation: complete for conversation=%s', conv.reference.conversation.id)
378
+ }
379
+
380
+ // ---------------------------------------------------------------------------
381
+ // Create new conversation
382
+ // ---------------------------------------------------------------------------
383
+
384
+ /**
385
+ * Creates a new conversation using the specified channel adapter and conversation options.
386
+ *
387
+ * @remarks
388
+ * This wraps `CloudAdapter.createConversationAsync()`, which requires real network connectivity
389
+ * and authentication. The provided adapter must implement
390
+ * `createConversationAsync()`; a `TypeError` is thrown if it does not.
391
+ *
392
+ * If `createOptions.storeConversation` is `true`, the resulting {@link Conversation} is
393
+ * automatically stored via {@link storeConversation} so it can be retrieved by ID later.
394
+ *
395
+ * If a `handler` is provided it is executed within the newly created conversation, giving the
396
+ * agent a chance to send an initial message or load state.
397
+ *
398
+ * @param adapter - The channel adapter used to create the conversation. Must implement
399
+ * `createConversationAsync()` (i.e. a `CloudAdapter` instance).
400
+ * @param createOptions - Details required to create the conversation, including identity, channel,
401
+ * service URL, OAuth scope, and `ConversationParameters`. Build with
402
+ * {@link CreateConversationOptionsBuilder}.
403
+ * @param handler - Optional route handler executed immediately after the conversation is created.
404
+ * @returns The newly created {@link Conversation}.
405
+ * @throws `TypeError` if the adapter does not implement `createConversationAsync()`.
406
+ * @example
407
+ * ```typescript
408
+ * // Initiate a new 1:1 conversation with a Teams user and send a welcome message
409
+ * const opts = CreateConversationOptionsBuilder
410
+ * .create(process.env.APP_ID!, 'msteams')
411
+ * .withUser('user-aad-object-id')
412
+ * .withTenantId('tenant-id')
413
+ * .storeConversation(true)
414
+ * .build()
415
+ *
416
+ * const conv = await app.proactive.createConversation(adapter, opts, async (ctx, state) => {
417
+ * await ctx.sendActivity('Hi! I have an update for you.')
418
+ * })
419
+ * console.log('New conversation ID:', conv.reference.conversation.id)
420
+ * ```
421
+ */
422
+ async createConversation (
423
+ adapter: BaseAdapter,
424
+ createOptions: CreateConversationOptions,
425
+ handler?: RouteHandler<TState>
426
+ ): Promise<Conversation> {
427
+ if (!createOptions.parameters.members?.length) {
428
+ throw ExceptionHelper.generateException(Error, Errors.ProactiveMembersRequired)
429
+ }
430
+
431
+ // CloudAdapter.createConversationAsync(agentAppId, channelId, serviceUrl, audience, params, logic)
432
+ // The logic callback IS the handler — context is created internally by the adapter.
433
+ const cloudAdapter = adapter as any
434
+ if (typeof cloudAdapter.createConversationAsync !== 'function') {
435
+ throw ExceptionHelper.generateException(TypeError, Errors.ProactiveCloudAdapterRequired)
436
+ }
437
+ logger.info('createConversation: channel=%s serviceUrl=%s members=%d',
438
+ createOptions.channelId, createOptions.serviceUrl, createOptions.parameters.members?.length ?? 0)
439
+
440
+ let capturedConv: Conversation | undefined
441
+ let caughtError: unknown
442
+
443
+ await cloudAdapter.createConversationAsync(
444
+ createOptions.identity.aud,
445
+ createOptions.channelId,
446
+ createOptions.serviceUrl,
447
+ createOptions.scope,
448
+ createOptions.parameters,
449
+ async (ctx: TurnContext) => {
450
+ try {
451
+ const conv = new Conversation(createOptions.identity, ctx.activity.getConversationReference())
452
+ capturedConv = conv
453
+ logger.debug('createConversation: created conversation=%s', conv.reference.conversation.id)
454
+
455
+ if (createOptions.storeConversation) {
456
+ await this.storeConversation(conv)
457
+ }
458
+
459
+ if (handler) {
460
+ const state = this._app.options.turnStateFactory()
461
+ await state.load(ctx, this.requireAppStorage())
462
+ await handler(ctx, state)
463
+ await state.save(ctx, this.requireAppStorage())
464
+ }
465
+ } catch (err) {
466
+ caughtError = err
467
+ }
468
+ }
469
+ )
470
+
471
+ if (caughtError !== undefined) {
472
+ logger.warn('createConversation: failed for channel=%s: %s', createOptions.channelId, caughtError)
473
+ throw caughtError
474
+ }
475
+
476
+ if (!capturedConv) {
477
+ throw ExceptionHelper.generateException(Error, Errors.ProactiveCallbackNotInvoked)
478
+ }
479
+ return capturedConv
480
+ }
481
+ }