@microsoft/agents-hosting 1.9.0-beta.3.gb2d23c1c44 → 1.9.1

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 (150) hide show
  1. package/README.md +182 -4
  2. package/dist/package.json +3 -3
  3. package/dist/src/app/agentApplication.d.ts +4 -3
  4. package/dist/src/app/agentApplication.js +24 -21
  5. package/dist/src/app/agentApplication.js.map +1 -1
  6. package/dist/src/app/agentApplicationBuilder.d.ts +9 -2
  7. package/dist/src/app/agentApplicationBuilder.js +9 -0
  8. package/dist/src/app/agentApplicationBuilder.js.map +1 -1
  9. package/dist/src/app/agentApplicationOptions.d.ts +16 -4
  10. package/dist/src/app/auth/authorization.d.ts +54 -0
  11. package/dist/src/app/auth/authorization.js +63 -2
  12. package/dist/src/app/auth/authorization.js.map +1 -1
  13. package/dist/src/app/auth/authorizationManager.d.ts +0 -8
  14. package/dist/src/app/auth/authorizationManager.js +104 -178
  15. package/dist/src/app/auth/authorizationManager.js.map +1 -1
  16. package/dist/src/app/auth/handlerStorage.d.ts +5 -3
  17. package/dist/src/app/auth/handlerStorage.js +36 -10
  18. package/dist/src/app/auth/handlerStorage.js.map +1 -1
  19. package/dist/src/app/auth/types.d.ts +2 -2
  20. package/dist/src/app/index.d.ts +1 -0
  21. package/dist/src/app/proactive/proactive.js +10 -6
  22. package/dist/src/app/proactive/proactive.js.map +1 -1
  23. package/dist/src/app/proactive/proactiveOptions.d.ts +2 -2
  24. package/dist/src/app/rateLimit.d.ts +315 -0
  25. package/dist/src/app/rateLimit.js +333 -0
  26. package/dist/src/app/rateLimit.js.map +1 -0
  27. package/dist/src/app/turnState.d.ts +6 -3
  28. package/dist/src/app/turnState.js +43 -4
  29. package/dist/src/app/turnState.js.map +1 -1
  30. package/dist/src/auth/authConfiguration.d.ts +10 -3
  31. package/dist/src/auth/authConfiguration.js +292 -259
  32. package/dist/src/auth/authConfiguration.js.map +1 -1
  33. package/dist/src/auth/delegatedTokenCredential.d.ts +29 -0
  34. package/dist/src/auth/delegatedTokenCredential.js +49 -0
  35. package/dist/src/auth/delegatedTokenCredential.js.map +1 -0
  36. package/dist/src/auth/msal/msalTokenCredential.js +1 -1
  37. package/dist/src/auth/msal/msalTokenCredential.js.map +1 -1
  38. package/dist/src/auth/settings.d.ts +8 -70
  39. package/dist/src/auth/settings.js +5 -67
  40. package/dist/src/auth/settings.js.map +1 -1
  41. package/dist/src/cloudAdapter.d.ts +5 -0
  42. package/dist/src/cloudAdapter.js +13 -103
  43. package/dist/src/cloudAdapter.js.map +1 -1
  44. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.d.ts +23 -0
  45. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js +183 -0
  46. package/dist/src/configuration/botFrameworkEnvironmentCompatibility.js.map +1 -0
  47. package/dist/src/configuration/configuration.d.ts +83 -0
  48. package/dist/src/configuration/configuration.js +836 -0
  49. package/dist/src/configuration/configuration.js.map +1 -0
  50. package/dist/src/configuration/configurationSource.d.ts +57 -0
  51. package/dist/src/configuration/configurationSource.js +7 -0
  52. package/dist/src/configuration/configurationSource.js.map +1 -0
  53. package/dist/src/configuration/environmentConfiguration.d.ts +14 -0
  54. package/dist/src/configuration/environmentConfiguration.js +271 -0
  55. package/dist/src/configuration/environmentConfiguration.js.map +1 -0
  56. package/dist/src/configuration/index.d.ts +7 -0
  57. package/dist/src/configuration/index.js +14 -0
  58. package/dist/src/configuration/index.js.map +1 -0
  59. package/dist/src/configuration/jsonFileConfigurationSource.d.ts +15 -0
  60. package/dist/src/configuration/jsonFileConfigurationSource.js +58 -0
  61. package/dist/src/configuration/jsonFileConfigurationSource.js.map +1 -0
  62. package/dist/src/createCloudAdapter.d.ts +30 -5
  63. package/dist/src/createCloudAdapter.js +17 -10
  64. package/dist/src/createCloudAdapter.js.map +1 -1
  65. package/dist/src/errorHelper.js +124 -5
  66. package/dist/src/errorHelper.js.map +1 -1
  67. package/dist/src/index.d.ts +1 -0
  68. package/dist/src/index.js +1 -0
  69. package/dist/src/index.js.map +1 -1
  70. package/dist/src/observability/traces.d.ts +6 -4
  71. package/dist/src/observability/traces.js +10 -9
  72. package/dist/src/observability/traces.js.map +1 -1
  73. package/dist/src/outboundHostValidator.d.ts +4 -1
  74. package/dist/src/outboundHostValidator.js +25 -48
  75. package/dist/src/outboundHostValidator.js.map +1 -1
  76. package/dist/src/state/agentState.d.ts +8 -3
  77. package/dist/src/state/agentState.js +26 -8
  78. package/dist/src/state/agentState.js.map +1 -1
  79. package/dist/src/state/conversationState.d.ts +2 -2
  80. package/dist/src/state/conversationState.js.map +1 -1
  81. package/dist/src/state/userState.d.ts +2 -2
  82. package/dist/src/state/userState.js.map +1 -1
  83. package/dist/src/statusCodes.d.ts +5 -0
  84. package/dist/src/statusCodes.js +5 -0
  85. package/dist/src/statusCodes.js.map +1 -1
  86. package/dist/src/storage/fileStorage.d.ts +83 -68
  87. package/dist/src/storage/fileStorage.js +279 -105
  88. package/dist/src/storage/fileStorage.js.map +1 -1
  89. package/dist/src/storage/index.d.ts +1 -0
  90. package/dist/src/storage/index.js +1 -0
  91. package/dist/src/storage/index.js.map +1 -1
  92. package/dist/src/storage/memoryStorage.d.ts +85 -60
  93. package/dist/src/storage/memoryStorage.js +244 -87
  94. package/dist/src/storage/memoryStorage.js.map +1 -1
  95. package/dist/src/storage/storage.d.ts +100 -2
  96. package/dist/src/storage/storage.js +25 -0
  97. package/dist/src/storage/storage.js.map +1 -1
  98. package/dist/src/storage/storageCompatibility.d.ts +17 -0
  99. package/dist/src/storage/storageCompatibility.js +157 -0
  100. package/dist/src/storage/storageCompatibility.js.map +1 -0
  101. package/dist/src/storage/storageExpiry.d.ts +6 -0
  102. package/dist/src/storage/storageExpiry.js +20 -0
  103. package/dist/src/storage/storageExpiry.js.map +1 -0
  104. package/dist/src/utils/env.d.ts +43 -0
  105. package/dist/src/utils/env.js +42 -6
  106. package/dist/src/utils/env.js.map +1 -1
  107. package/dist/src/utils.d.ts +1 -0
  108. package/dist/src/utils.js +10 -1
  109. package/dist/src/utils.js.map +1 -1
  110. package/package.json +3 -3
  111. package/src/app/agentApplication.ts +27 -14
  112. package/src/app/agentApplicationBuilder.ts +13 -2
  113. package/src/app/agentApplicationOptions.ts +18 -4
  114. package/src/app/auth/authorization.ts +67 -2
  115. package/src/app/auth/authorizationManager.ts +126 -204
  116. package/src/app/auth/handlerStorage.ts +51 -7
  117. package/src/app/auth/types.ts +2 -2
  118. package/src/app/index.ts +7 -0
  119. package/src/app/proactive/proactive.ts +19 -9
  120. package/src/app/proactive/proactiveOptions.ts +2 -2
  121. package/src/app/rateLimit.ts +659 -0
  122. package/src/app/turnState.ts +50 -7
  123. package/src/auth/authConfiguration.ts +415 -260
  124. package/src/auth/delegatedTokenCredential.ts +49 -0
  125. package/src/auth/msal/msalTokenCredential.ts +1 -1
  126. package/src/auth/settings.ts +5 -82
  127. package/src/cloudAdapter.ts +38 -113
  128. package/src/configuration/botFrameworkEnvironmentCompatibility.ts +245 -0
  129. package/src/configuration/configuration.ts +1076 -0
  130. package/src/configuration/configurationSource.ts +76 -0
  131. package/src/configuration/environmentConfiguration.ts +377 -0
  132. package/src/configuration/index.ts +19 -0
  133. package/src/configuration/jsonFileConfigurationSource.ts +74 -0
  134. package/src/createCloudAdapter.ts +52 -9
  135. package/src/errorHelper.ts +151 -5
  136. package/src/index.ts +1 -0
  137. package/src/observability/traces.ts +8 -7
  138. package/src/outboundHostValidator.ts +37 -44
  139. package/src/state/agentState.ts +39 -13
  140. package/src/state/conversationState.ts +2 -2
  141. package/src/state/userState.ts +2 -2
  142. package/src/statusCodes.ts +6 -0
  143. package/src/storage/fileStorage.ts +299 -106
  144. package/src/storage/index.ts +1 -0
  145. package/src/storage/memoryStorage.ts +268 -85
  146. package/src/storage/storage.ts +114 -2
  147. package/src/storage/storageCompatibility.ts +185 -0
  148. package/src/storage/storageExpiry.ts +21 -0
  149. package/src/utils/env.ts +63 -0
  150. package/src/utils.ts +10 -0
@@ -3,66 +3,94 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { trace } from '@microsoft/agents-telemetry'
7
6
  import { ExceptionHelper } from '@microsoft/agents-activity'
8
- import { StorageTraceDefinitions } from '../observability'
9
- import { Storage, StoreItem } from './storage'
10
- import { debug } from '@microsoft/agents-telemetry'
7
+ import { debug, redactString, trace } from '@microsoft/agents-telemetry'
11
8
  import { Errors } from '../errorHelper'
9
+ import { StorageTraceDefinitions } from '../observability'
10
+ import { getStorageWriteExpiry } from './storageExpiry'
11
+ import {
12
+ StorageDeleteOptions,
13
+ StorageDeleteResults,
14
+ StorageOperationStatus,
15
+ StorageReadResults,
16
+ StorageWriteMode,
17
+ StorageWriteOptions,
18
+ StorageWriteResults,
19
+ Storage,
20
+ StorageV2,
21
+ StoreItem,
22
+ } from './storage'
12
23
 
13
24
  const logger = debug('agents:memory-storage')
14
25
 
15
- /**
16
- * A simple in-memory storage provider that implements the Storage interface.
17
- *
18
- * @remarks
19
- * This class provides a volatile storage solution that keeps data in memory,
20
- * which means data is lost when the process terminates. It's primarily useful for:
21
- * - Development and testing scenarios
22
- * - Simple applications that don't require data persistence across restarts
23
- * - Stateless environments where external storage isn't available
24
- *
25
- * MemoryStorage supports optimistic concurrency control through eTags and
26
- * can be used as a singleton through the {@link MemoryStorage.getSingleInstance | getSingleInstance() method} to
27
- * share state across different parts of an application.
28
- */
29
- export class MemoryStorage implements Storage {
30
- private static instance: MemoryStorage
31
- /**
32
- * Counter used to generate unique eTags for stored items
33
- */
34
- private etag: number = 1
26
+ interface MemoryStorageState {
27
+ memory: { [key: string]: string };
28
+ versions: { [key: string]: string };
29
+ expirations: { [key: string]: number };
30
+ etag: number;
31
+ }
35
32
 
36
- /**
37
- * Creates a new instance of the MemoryStorage class.
38
- *
39
- * @param memory An optional initial memory store to seed the storage with data
40
- */
41
- constructor (private memory: { [k: string]: string } = {}) { }
33
+ class MemoryStorageInternals {
34
+ private static readonly states = new WeakMap<object, MemoryStorageState>()
35
+
36
+ readonly state: MemoryStorageState
42
37
 
43
38
  /**
44
- * Gets a single shared instance of the MemoryStorage class.
45
- *
46
- * @returns The singleton instance of MemoryStorage
47
- *
48
- * @remarks
49
- * Using this method ensures that the same storage instance is used across
50
- * the application, allowing for shared state without passing references.
51
- *
39
+ * Creates an internal in-memory provider for the selected storage contract.
52
40
  */
53
- static getSingleInstance (): MemoryStorage {
54
- if (!MemoryStorage.instance) {
55
- MemoryStorage.instance = new MemoryStorage()
41
+ constructor (memory: { [key: string]: string } = {}) {
42
+ let state = MemoryStorageInternals.states.get(memory)
43
+ if (!state) {
44
+ state = { memory, versions: {}, expirations: {}, etag: getNextETag(memory) }
45
+ MemoryStorageInternals.states.set(memory, state)
56
46
  }
57
- return MemoryStorage.instance
47
+ this.state = state
48
+ }
49
+
50
+ save (key: string, item: unknown, expiresAt?: number): string {
51
+ const version = (this.state.etag++).toString()
52
+ this.state.memory[key] = JSON.stringify(item)
53
+ this.state.versions[key] = version
54
+ if (expiresAt === undefined) delete this.state.expirations[key]
55
+ else this.state.expirations[key] = expiresAt
56
+ return version
57
+ }
58
+
59
+ getVersion (key: string, value: StoreItem): string | undefined {
60
+ return this.state.versions[key] ?? value.eTag as string | undefined
61
+ }
62
+
63
+ isExpired (key: string): boolean {
64
+ const expiresAt = this.state.expirations[key]
65
+ return expiresAt !== undefined && expiresAt <= Date.now()
66
+ }
67
+
68
+ remove (key: string): void {
69
+ delete this.state.memory[key]
70
+ delete this.state.versions[key]
71
+ delete this.state.expirations[key]
72
+ }
73
+ }
74
+
75
+ /**
76
+ * A simple in-memory storage provider for development and testing.
77
+ *
78
+ * This class implements the legacy {@link Storage} contract. Use
79
+ * {@link MemoryStorageV2} for the structured StorageV2 contract.
80
+ */
81
+ export class MemoryStorage extends MemoryStorageInternals implements Storage {
82
+ private static instance: MemoryStorage
83
+
84
+ constructor (memory: { [key: string]: string } = {}) {
85
+ super(memory)
58
86
  }
59
87
 
60
88
  /**
61
- * Reads storage items from memory.
89
+ * Reads legacy items from process-local memory.
62
90
  *
63
- * @param keys The keys of the items to read
64
- * @returns A promise that resolves to the read items
65
- * @throws Will throw an error if keys are not provided or the array is empty
91
+ * @param keys The keys to read; must not be empty.
92
+ * @returns Existing items keyed by storage key. Missing keys are omitted and returned items
93
+ * include their legacy `eTag`.
66
94
  */
67
95
  async read (keys: string[]): Promise<StoreItem> {
68
96
  return trace(StorageTraceDefinitions.read, async ({ record }) => {
@@ -74,85 +102,240 @@ export class MemoryStorage implements Storage {
74
102
  const data: StoreItem = {}
75
103
  for (const key of keys) {
76
104
  logger.debug(`Reading key: ${key}`)
77
- const item = this.memory[key]
105
+ const item = this.state.memory[key]
78
106
  if (item) {
79
- data[key] = JSON.parse(item)
107
+ const value = JSON.parse(item)
108
+ const version = this.getVersion(key, value)
109
+ data[key] = version === undefined ? value : { ...value, eTag: version }
80
110
  }
81
111
  }
82
-
83
112
  return data
84
113
  })
85
114
  }
86
115
 
87
116
  /**
88
- * Writes storage items to memory.
89
- *
90
- * @param changes The items to write, indexed by key
91
- * @returns A promise that resolves when the write operation is complete
92
- * @throws Will throw an error if changes are not provided or if there's an eTag conflict
117
+ * Writes legacy items to process-local memory.
93
118
  *
94
- * @remarks
95
- * This method supports optimistic concurrency control through eTags.
96
- * If an item has an eTag, it will only be updated if the existing item
97
- * has the same eTag. If an item has an eTag of '*' or no eTag, it will
98
- * always be written regardless of the current state.
119
+ * @param changes The items to write, keyed by storage key.
120
+ * @throws If `changes` is invalid or an item supplies a stale legacy `eTag`.
99
121
  */
100
122
  async write (changes: StoreItem): Promise<void> {
101
123
  return trace(StorageTraceDefinitions.write, async ({ record }) => {
102
- if (!changes || changes.length === 0) {
124
+ record({ keyCount: changes ? Object.keys(changes).length : undefined })
125
+ if (!changes || typeof changes !== 'object' || Array.isArray(changes)) {
103
126
  throw ExceptionHelper.generateException(ReferenceError, Errors.StorageWriteChangesRequired)
104
127
  }
105
- record({ keyCount: changes.length })
106
128
 
107
129
  for (const [key, newItem] of Object.entries(changes)) {
108
130
  logger.debug(`Writing key: ${key}`)
109
- const oldItemStr = this.memory[key]
131
+ const oldItemStr = this.state.memory[key]
110
132
  if (!oldItemStr || newItem.eTag === '*' || !newItem.eTag) {
111
- this.saveItem(key, newItem)
133
+ const { eTag: _eTag, ...value } = newItem
134
+ this.save(key, { ...value, eTag: (this.state.etag).toString() })
135
+ continue
136
+ }
137
+ const oldItem = JSON.parse(oldItemStr)
138
+ if (newItem.eTag === this.getVersion(key, oldItem)) {
139
+ const { eTag: _eTag, ...value } = newItem
140
+ this.save(key, { ...value, eTag: (this.state.etag).toString() })
112
141
  } else {
113
- const oldItem = JSON.parse(oldItemStr)
114
- if (newItem.eTag === oldItem.eTag) {
115
- this.saveItem(key, newItem)
116
- } else {
117
- throw ExceptionHelper.generateException(Error, Errors.StorageETagConflict, undefined, { key })
118
- }
142
+ throw ExceptionHelper.generateException(Error, Errors.StorageETagConflict, undefined, { key })
119
143
  }
120
144
  }
121
145
  })
122
146
  }
123
147
 
124
148
  /**
125
- * Deletes storage items from memory.
149
+ * Deletes legacy items from process-local memory.
126
150
  *
127
- * @param keys The keys of the items to delete
128
- * @returns A promise that resolves when the delete operation is complete
151
+ * @param keys The keys to delete. Missing keys are ignored.
129
152
  */
130
153
  async delete (keys: string[]): Promise<void> {
131
154
  return trace(StorageTraceDefinitions.delete, async ({ record }) => {
132
155
  record({ keyCount: keys?.length })
133
156
  logger.debug(`Deleting keys: ${keys.join(', ')}`)
134
157
  for (const key of keys) {
135
- delete this.memory[key]
158
+ this.remove(key)
136
159
  }
137
160
  })
138
161
  }
139
162
 
163
+ static getSingleInstance (): MemoryStorage {
164
+ if (!MemoryStorage.instance) MemoryStorage.instance = new MemoryStorage()
165
+ return MemoryStorage.instance
166
+ }
167
+ }
168
+
169
+ /**
170
+ * An in-memory provider for the structured {@link StorageV2} contract.
171
+ *
172
+ * Unlike {@link MemoryStorage}, every read returns an outcome for every requested key, and every
173
+ * write and delete returns an outcome for every supplied key. Storage versions are returned
174
+ * separately from application values, so an `eTag` property in a value is preserved. Use
175
+ * {@link StorageWriteMode.CreateOnly}, {@link StorageWriteMode.Replace}, or `expectedVersion` to
176
+ * guard concurrent writes.
177
+ *
178
+ * This provider is intended for development and testing only; state is process-local.
179
+ */
180
+ export class MemoryStorageV2 extends StorageV2 {
181
+ private static instance: MemoryStorageV2
182
+ private readonly internals: MemoryStorageInternals
183
+
140
184
  /**
141
- * Saves an item to memory with a new eTag.
185
+ * Creates a V2 in-memory storage provider.
142
186
  *
143
- * @param key The key of the item to save
144
- * @param item The item to save
187
+ * @param memory Optional backing store to share with another in-memory storage instance.
188
+ */
189
+ constructor (memory: { [key: string]: string } = {}) {
190
+ super()
191
+ this.internals = new MemoryStorageInternals(memory)
192
+ }
193
+
194
+ /**
195
+ * Reads items from process-local memory.
145
196
  *
146
- * @remarks
147
- * This private method handles the details of:
148
- * - Creating a clone of the item to prevent modification of the original
149
- * - Generating a new eTag for optimistic concurrency control
150
- * - Converting the item to a JSON string for storage
197
+ * @param keys The keys to read. Empty batches are valid.
198
+ * @returns A result for every key, with `notFound` for missing items and a separate storage
199
+ * version for successful reads.
200
+ */
201
+ async read<T extends object = Record<string, unknown>> (keys: string[]): Promise<StorageReadResults<T>> {
202
+ return trace(StorageTraceDefinitions.read, async ({ record }) => {
203
+ record({ keyCount: keys?.length })
204
+ if (!Array.isArray(keys)) {
205
+ throw ExceptionHelper.generateException(ReferenceError, Errors.StorageReadKeysRequired)
206
+ }
207
+ if (keys.some(key => typeof key !== 'string' || key.trim() === '')) {
208
+ throw ExceptionHelper.generateException(ReferenceError, Errors.StorageV2KeyRequired)
209
+ }
210
+
211
+ const results: StorageReadResults<T> = {}
212
+ for (const key of keys) {
213
+ logger.debug(`Reading key: ${key}`)
214
+ if (this.internals.isExpired(key)) {
215
+ logger.info('Item expired, deleting from memory', { key: redactString(key, true) })
216
+ this.internals.remove(key)
217
+ }
218
+ const item = this.internals.state.memory[key]
219
+ if (!item) {
220
+ results[key] = { key, status: StorageOperationStatus.NotFound }
221
+ continue
222
+ }
223
+ const value = JSON.parse(item) as T & StoreItem
224
+ results[key] = { key, status: StorageOperationStatus.Succeeded, value, version: this.internals.getVersion(key, value) }
225
+ }
226
+ return results
227
+ })
228
+ }
229
+
230
+ /**
231
+ * Writes items to process-local memory with optional concurrency conditions.
232
+ *
233
+ * @param changes The values to write, keyed by storage key.
234
+ * @param options Create-only, replace, or expected-version conditions.
235
+ * @returns A result for every supplied key, including `conflict` or `conditionNotMet` when a
236
+ * condition cannot be satisfied.
237
+ */
238
+ async write<T extends object = Record<string, unknown>> (changes: Record<string, T>, options?: StorageWriteOptions): Promise<StorageWriteResults> {
239
+ return trace(StorageTraceDefinitions.write, async ({ record }) => {
240
+ record({ keyCount: changes ? Object.keys(changes).length : undefined })
241
+ if (options?.expectedVersion === '') {
242
+ throw ExceptionHelper.generateException(RangeError, Errors.StorageV2ExpectedVersionEmpty)
243
+ }
244
+ if (!changes || typeof changes !== 'object' || Array.isArray(changes)) {
245
+ throw ExceptionHelper.generateException(ReferenceError, Errors.StorageWriteChangesRequired)
246
+ }
247
+ if (Object.values(changes).some(value => value === null || typeof value !== 'object' || Array.isArray(value))) {
248
+ throw ExceptionHelper.generateException(TypeError, Errors.StorageV2ValueRequired)
249
+ }
250
+ if (Object.keys(changes).some(key => key.trim() === '')) {
251
+ throw ExceptionHelper.generateException(ReferenceError, Errors.StorageV2KeyRequired)
252
+ }
253
+
254
+ const results: StorageWriteResults = {}
255
+ const expiresAt = getStorageWriteExpiry(options)
256
+ const mode = options?.mode ?? StorageWriteMode.Upsert
257
+ if (!Object.values(StorageWriteMode).includes(mode)) {
258
+ throw ExceptionHelper.generateException(RangeError, Errors.StorageV2WriteModeUnsupported, undefined, { mode: String(mode) })
259
+ }
260
+ for (const [key, newItem] of Object.entries(changes)) {
261
+ if (this.internals.isExpired(key)) {
262
+ logger.info('Item expired, deleting from memory', { key: redactString(key, true) })
263
+ this.internals.remove(key)
264
+ }
265
+ const oldItemStr = this.internals.state.memory[key]
266
+ const oldItem = oldItemStr ? JSON.parse(oldItemStr) as StoreItem : undefined
267
+ const currentVersion = oldItem ? this.internals.getVersion(key, oldItem) : undefined
268
+ if (mode === StorageWriteMode.CreateOnly && oldItemStr) {
269
+ results[key] = { key, status: StorageOperationStatus.Conflict, version: currentVersion }
270
+ } else if (mode === StorageWriteMode.Replace && !oldItemStr) {
271
+ results[key] = { key, status: StorageOperationStatus.NotFound }
272
+ } else if (options?.expectedVersion !== undefined && options.expectedVersion !== currentVersion) {
273
+ results[key] = { key, status: StorageOperationStatus.ConditionNotMet, version: currentVersion }
274
+ } else {
275
+ results[key] = { key, status: StorageOperationStatus.Succeeded, version: this.internals.save(key, newItem, expiresAt) }
276
+ }
277
+ }
278
+ return results
279
+ })
280
+ }
281
+
282
+ /**
283
+ * Deletes items from process-local memory with an optional version condition.
151
284
  *
152
- * @private
285
+ * @param keys The keys to delete. Empty batches are valid.
286
+ * @param options An optional expected storage version.
287
+ * @returns A result for every supplied key.
153
288
  */
154
- private saveItem (key: string, item: unknown): void {
155
- const clone = Object.assign({}, item, { eTag: (this.etag++).toString() })
156
- this.memory[key] = JSON.stringify(clone)
289
+ async delete (keys: string[], options?: StorageDeleteOptions): Promise<StorageDeleteResults> {
290
+ return trace(StorageTraceDefinitions.delete, async ({ record }) => {
291
+ record({ keyCount: keys?.length })
292
+ if (options?.expectedVersion === '') {
293
+ throw ExceptionHelper.generateException(RangeError, Errors.StorageV2ExpectedVersionEmpty)
294
+ }
295
+ if (!Array.isArray(keys)) {
296
+ throw ExceptionHelper.generateException(ReferenceError, Errors.StorageReadKeysRequired)
297
+ }
298
+ if (keys.some(key => typeof key !== 'string' || key.trim() === '')) {
299
+ throw ExceptionHelper.generateException(ReferenceError, Errors.StorageV2KeyRequired)
300
+ }
301
+
302
+ const results: StorageDeleteResults = {}
303
+ for (const key of keys) {
304
+ if (this.internals.isExpired(key)) {
305
+ logger.info('Item expired, deleting from memory', { key: redactString(key, true) })
306
+ this.internals.remove(key)
307
+ }
308
+ const item = this.internals.state.memory[key]
309
+ if (!item) {
310
+ results[key] = { key, status: StorageOperationStatus.NotFound }
311
+ continue
312
+ }
313
+ const value = JSON.parse(item) as StoreItem
314
+ const version = this.internals.getVersion(key, value)
315
+ if (options?.expectedVersion !== undefined && options.expectedVersion !== version) {
316
+ results[key] = { key, status: StorageOperationStatus.ConditionNotMet, version }
317
+ continue
318
+ }
319
+ this.internals.remove(key)
320
+ results[key] = { key, status: StorageOperationStatus.Succeeded, version }
321
+ }
322
+ return results
323
+ })
324
+ }
325
+
326
+ static getSingleInstance (): MemoryStorageV2 {
327
+ if (!MemoryStorageV2.instance) MemoryStorageV2.instance = new MemoryStorageV2()
328
+ return MemoryStorageV2.instance
157
329
  }
158
330
  }
331
+
332
+ function getNextETag (memory: { [key: string]: string }): number {
333
+ return Object.values(memory).reduce((next, item) => {
334
+ try {
335
+ const version = Number(JSON.parse(item)?.eTag)
336
+ return Number.isSafeInteger(version) && version >= next ? version + 1 : next
337
+ } catch {
338
+ return next
339
+ }
340
+ }, 1)
341
+ }
@@ -63,7 +63,7 @@ export interface StoreItems {
63
63
  export type StorageKeyFactory = (context: TurnContext) => string | Promise<string>
64
64
 
65
65
  /**
66
- * Defines the interface for storage operations in the Agents platform.
66
+ * Defines the version 1 interface for storage operations in the Agents platform.
67
67
  *
68
68
  * @remarks
69
69
  * Storage providers persist state data across conversation turns, enabling
@@ -72,7 +72,9 @@ export type StorageKeyFactory = (context: TurnContext) => string | Promise<strin
72
72
  *
73
73
  * The interface is designed to be simple with just three core operations:
74
74
  * read, write, and delete. All operations are asynchronous to support both
75
- * in-memory and remote storage providers.
75
+ * in-memory and remote storage providers. New implementations should use
76
+ * {@link StorageV2}; this V1 interface remains supported by
77
+ * {@link StorageProvider} through a compatibility adapter.
76
78
  */
77
79
  export interface Storage {
78
80
  /**
@@ -101,3 +103,113 @@ export interface Storage {
101
103
  */
102
104
  delete: (keys: string[]) => Promise<void>;
103
105
  }
106
+
107
+ /**
108
+ * The result of a version 2 storage read operation.
109
+ *
110
+ * A result is returned for every requested key. `value` and `version` are
111
+ * available when `status` is {@link StorageOperationStatus.Succeeded}.
112
+ */
113
+ export interface StorageReadResult<T extends object = Record<string, unknown>> {
114
+ key: string;
115
+ status: StorageOperationStatus;
116
+ value?: T;
117
+ version?: string;
118
+ }
119
+
120
+ /**
121
+ * The result of a version 2 storage write operation.
122
+ */
123
+ export interface StorageWriteResult {
124
+ key: string;
125
+ status: StorageOperationStatus;
126
+ version?: string;
127
+ }
128
+
129
+ /**
130
+ * The result of a version 2 storage delete operation.
131
+ */
132
+ export interface StorageDeleteResult {
133
+ key: string;
134
+ status: StorageOperationStatus;
135
+ version?: string;
136
+ }
137
+
138
+ /** A keyed set of storage read results. */
139
+ export type StorageReadResults<T extends object = Record<string, unknown>> = Record<string, StorageReadResult<T>>
140
+
141
+ /** A keyed set of storage write results. */
142
+ export type StorageWriteResults = Record<string, StorageWriteResult>
143
+
144
+ /** A keyed set of storage delete results. */
145
+ export type StorageDeleteResults = Record<string, StorageDeleteResult>
146
+
147
+ /** The outcome of one version 2 storage operation. */
148
+ export enum StorageOperationStatus {
149
+ Succeeded = 'succeeded',
150
+ NotFound = 'notFound',
151
+ Conflict = 'conflict',
152
+ ConditionNotMet = 'conditionNotMet',
153
+ }
154
+
155
+ /** The write mode for a version 2 storage operation. */
156
+ export enum StorageWriteMode {
157
+ Upsert = 'upsert',
158
+ CreateOnly = 'createOnly',
159
+ Replace = 'replace',
160
+ }
161
+
162
+ /** Options applied to every item in a version 2 write operation. */
163
+ export interface StorageWriteOptions {
164
+ /**
165
+ * Optional time to live, in seconds, for written items.
166
+ *
167
+ * When set, storage providers stop returning the written items after this
168
+ * duration has elapsed. Values must be finite numbers greater than zero.
169
+ */
170
+ ttl?: number;
171
+ mode?: StorageWriteMode;
172
+ expectedVersion?: string;
173
+ }
174
+
175
+ /** Options applied to every item in a version 2 delete operation. */
176
+ export interface StorageDeleteOptions {
177
+ expectedVersion?: string;
178
+ }
179
+
180
+ /**
181
+ * The version 2 storage contract.
182
+ *
183
+ * This does not extend {@link Storage}: JavaScript cannot overload methods by
184
+ * return type at runtime. V2 providers are exposed as separately named classes.
185
+ */
186
+ export abstract class StorageV2 {
187
+ /**
188
+ * Reads stored values and returns one result for every requested key.
189
+ *
190
+ * @param keys The keys to read. An empty array is valid.
191
+ * @returns Keyed results containing a value and storage version when found.
192
+ */
193
+ abstract read<T extends object = Record<string, unknown>> (keys: string[]): Promise<StorageReadResults<T>>
194
+
195
+ /**
196
+ * Writes values with optional create, replace, or version conditions.
197
+ *
198
+ * @param changes Values indexed by their storage keys.
199
+ * @param options Conditions applied to every value in the batch.
200
+ * @returns One result for every supplied key.
201
+ */
202
+ abstract write<T extends object = Record<string, unknown>> (changes: Record<string, T>, options?: StorageWriteOptions): Promise<StorageWriteResults>
203
+
204
+ /**
205
+ * Deletes values with an optional version condition.
206
+ *
207
+ * @param keys The keys to delete. An empty array is valid.
208
+ * @param options Conditions applied to every key in the batch.
209
+ * @returns One result for every supplied key.
210
+ */
211
+ abstract delete (keys: string[], options?: StorageDeleteOptions): Promise<StorageDeleteResults>
212
+ }
213
+
214
+ /** A storage implementation supported by public hosting interfaces. */
215
+ export type StorageProvider = Storage | StorageV2