UNPKG

@microsoft/agents-hosting

Version:

Microsoft 365 Agents SDK for JavaScript

425 lines (414 loc) 15.8 kB
/** * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ import { TurnContext } from '../turnContext' import { AgentState, CustomKey } from './agentState' /** * Interface for accessing a property in state storage with type safety. * * @typeParam T The type of the property being accessed * * @remarks * This interface defines standard methods for working with persisted state properties, * allowing property access with strong typing to reduce errors when working with * complex state objects. * */ export interface StatePropertyAccessor<T = any> { /** * Deletes the persisted property from its backing storage object. * * @param context Context for the current turn of conversation with the user. * * @remarks * The properties backing storage object SHOULD be loaded into memory on first access. * * @example * ```javascript * await myProperty.delete(context); * ``` * */ delete(context: TurnContext): Promise<void>; /** * Reads a persisted property from its backing storage object. * * @param context Context for the current turn of conversation with the user. * * @remarks * The properties backing storage object SHOULD be loaded into memory on first access. * * If the property does not currently exist on the storage object and a `defaultValue` has been * specified, a clone of the `defaultValue` SHOULD be copied to the storage object. If a * `defaultValue` has not been specified then a value of `undefined` SHOULD be returned. * * @example * ```javascript * const value = await myProperty.get(context, { count: 0 }); * ``` * */ get(context: TurnContext): Promise<T | undefined>; /** * Reads a persisted property from its backing storage object. * * @param context Context for the current turn of conversation with the user. * @param defaultValue (Optional) default value to copy to the backing storage object if the property isn't found. */ get(context: TurnContext, defaultValue: T): Promise<T>; /** * Assigns a new value to the properties backing storage object. * * @param context Context for the current turn of conversation with the user. * @param value Value to assign. * * @remarks * The properties backing storage object SHOULD be loaded into memory on first access. * * Depending on the state systems implementation, an additional step may be required to * persist the actual changes to disk. * * @example * ```javascript * await myProperty.set(context, value); * ``` * */ set(context: TurnContext, value: T): Promise<void>; } /** * Provides typed access to an Agent state property with automatic state loading and persistence management. * * @typeParam T The type of the property being accessed. Can be any serializable type. * * @remarks * `AgentStatePropertyAccessor` simplifies working with persisted state by abstracting * the complexity of loading state from storage and manipulating specific properties. * It provides a type-safe interface for state management with automatic handling of: * * - **Lazy Loading**: State is loaded from storage only when first accessed * - **Type Safety**: Full TypeScript support with generic type parameters * - **Default Values**: Automatic deep cloning of default values to prevent reference issues * - **Memory Management**: Efficient in-memory caching with explicit persistence control * - **Custom Keys**: Support for custom storage keys for advanced scenarios * * ### Key Features * * Key features of `AgentStatePropertyAccessor` include: * - [Type Safety](#type-safety) * - [Automatic Default Value Handling](#automatic-default-value-handling) * - [Explicit Persistence Control](#explicit-persistence-control) * * #### Type Safety * The accessor provides compile-time type checking when using TypeScript: * ```typescript * interface UserProfile { * name: string; * preferences: { theme: string; language: string }; * } * const userProfile = userState.createProperty<UserProfile>("userProfile"); * ``` * * #### Automatic Default Value Handling * When a property doesn't exist, default values are automatically cloned and stored: * ```typescript * // If userProfile doesn't exist, the default will be cloned and saved * const profile = await userProfile.get(context, { * name: "Anonymous", * preferences: { theme: "light", language: "en" } * }); * ``` * * #### Explicit Persistence Control * Changes are kept in memory until explicitly persisted: * ```typescript * // Modify the state * const counter = await counterProperty.get(context, 0); * await counterProperty.set(context, counter + 1); * * // Changes are only in memory at this point * // Persist to storage * await userState.saveChanges(context); * ``` * * ### Usage Examples * * @example Basic Usage * ```typescript * // Create a property accessor * const userProfile = userState.createProperty<UserProfile>("userProfile"); * * // Get with default value * const profile = await userProfile.get(context, { * name: "", * preferences: { theme: "light", language: "en" } * }); * * // Modify the profile * profile.preferences.theme = "dark"; * * // Save the changes * await userProfile.set(context, profile); * await userState.saveChanges(context); // Persist to storage * ``` * * @example Working with Primitive Types * ```typescript * const counterProperty = userState.createProperty<number>("counter"); * * // Increment counter * const currentCount = await counterProperty.get(context, 0); * await counterProperty.set(context, currentCount + 1); * await userState.saveChanges(context); * ``` * * @example Conditional Logic * ```typescript * const settingsProperty = userState.createProperty<Settings>("settings"); * * // Check if property exists * const settings = await settingsProperty.get(context); * if (settings === undefined) { * // Property doesn't exist, initialize with defaults * await settingsProperty.set(context, getDefaultSettings()); * } * ``` * * @example Custom Storage Keys * ```typescript * // Store state with a custom key for multi-tenant scenarios * const customKey = { key: `tenant_${tenantId}` }; * const tenantData = await dataProperty.get(context, defaultData, customKey); * await dataProperty.set(context, updatedData, customKey); * ``` * * ### Important Notes * * - **Thread Safety**: This class is not thread-safe. Ensure proper synchronization in concurrent scenarios. * - **Memory Usage**: State objects are kept in memory until the context is disposed. * - **Persistence**: Always call `state.saveChanges(context)` to persist changes to storage. * - **Deep Cloning**: Default values are deep cloned using JSON serialization, which may not work with complex objects containing functions or circular references. * * @see {@link AgentState.createProperty} for creating property accessors * @see {@link StatePropertyAccessor} for the interface definition */ export class AgentStatePropertyAccessor<T = any> implements StatePropertyAccessor<T> { /** * Creates a new instance of AgentStatePropertyAccessor. * * @param state The agent state object that manages the backing storage for this property * @param name The unique name of the property within the state object. This name is used as the key in the state storage. * * @remarks * This constructor is typically not called directly. Instead, use {@link AgentState.createProperty} * to create property accessors, which ensures proper integration with the state management system. * * @example * ```typescript * // Recommended way - use AgentState.createProperty * const userProfile = userState.createProperty<UserProfile>("userProfile"); * * // Direct construction (not recommended) * const accessor = new AgentStatePropertyAccessor<UserProfile>(userState, "userProfile"); * ``` * */ constructor (protected readonly state: AgentState, public readonly name: string) { } /** * Deletes the property from the state storage. * * @param context The turn context for the current conversation turn * @param customKey Optional custom key for accessing state in a specific storage location. * Useful for multi-tenant scenarios or when state needs to be partitioned. * @returns A promise that resolves when the delete operation is complete * * @remarks * This operation removes the property from the in-memory state object but does not * automatically persist the change to the underlying storage. You must call * `state.saveChanges(context)` afterwards to persist the deletion. * * - If the property doesn't exist, this operation is a no-op * - The deletion only affects the in-memory state until `saveChanges()` is called * - After deletion, subsequent `get()` calls will return `undefined` (or the default value if provided) * * @example Basic usage * ```typescript * const userSettings = userState.createProperty<UserSettings>("settings"); * * // Delete the user settings * await userSettings.delete(context); * * // Persist the deletion to storage * await userState.saveChanges(context); * * // Verify deletion * const settings = await userSettings.get(context); // Returns undefined * ``` * * @example Custom key usage * ```typescript * const tenantKey = { key: `tenant_${tenantId}` }; * await userSettings.delete(context, tenantKey); * await userState.saveChanges(context); * ``` * */ async delete (context: TurnContext, customKey?: CustomKey): Promise<void> { const obj: any = await this.state.load(context, false, customKey) if (Object.prototype.hasOwnProperty.call(obj, this.name)) { delete obj[this.name] } } /** * Retrieves the value of the property from state storage. * * @param context The turn context for the current conversation turn * @param defaultValue Optional default value to use if the property doesn't exist. * When provided, this value is deep cloned and stored in state. * @param customKey Optional custom key for accessing state in a specific storage location. * Useful for multi-tenant scenarios or when state needs to be partitioned. * * @returns A promise that resolves to the property value, the cloned default value, or `undefined` * * @remarks * This method provides intelligent default value handling: * - If the property exists, its value is returned * - If the property doesn't exist and a default value is provided, the default is deep cloned, * stored in state, and returned * - If the property doesn't exist and no default is provided, `undefined` is returned * * **Deep Cloning**: Default values are deep cloned using JSON serialization to prevent * reference sharing issues. This means: * - Functions, symbols, and circular references will be lost * - Dates become strings (use Date constructor to restore) * - Complex objects with prototypes lose their prototype chain * * **Performance**: The first access loads state from storage; subsequent accesses use * the in-memory cached version until the context is disposed. * * @example Basic usage * ```typescript * const counterProperty = userState.createProperty<number>("counter"); * * // Get with default value * const count = await counterProperty.get(context, 0); * console.log(count); // 0 if property doesn't exist, otherwise the stored value * ``` * * @example Complex object with default * ```typescript * interface UserProfile { * name: string; * preferences: { theme: string; notifications: boolean }; * } * * const userProfile = userState.createProperty<UserProfile>("profile"); * const profile = await userProfile.get(context, { * name: "Anonymous", * preferences: { theme: "light", notifications: true } * }); * ``` * * @example Checking for existence * ```typescript * const profile = await userProfile.get(context); * if (profile === undefined) { * console.log("Profile has not been set yet"); * } else { * console.log(`Welcome back, ${profile.name}!`); * } * ``` * * @example Custom key usage * ```typescript * const tenantKey = { key: `tenant_${tenantId}` }; * const tenantData = await dataProperty.get(context, defaultData, tenantKey); * ``` * */ async get (context: TurnContext, defaultValue?: T, customKey?: CustomKey): Promise<T> { const obj: any = await this.state.load(context, false, customKey) if (!Object.prototype.hasOwnProperty.call(obj, this.name) && defaultValue !== undefined) { const clone: any = typeof defaultValue === 'object' || Array.isArray(defaultValue) ? JSON.parse(JSON.stringify(defaultValue)) : defaultValue obj[this.name] = clone } return obj[this.name] } /** * Sets the value of the property in state storage. * * @param context The turn context for the current conversation turn * @param value The value to assign to the property. Can be any serializable value. * @param customKey Optional custom key for accessing state in a specific storage location. * Useful for multi-tenant scenarios or when state needs to be partitioned. * * @returns A promise that resolves when the set operation is complete * * @remarks * This operation updates the property in the in-memory state object but does not * automatically persist the change to the underlying storage. You must call * `state.saveChanges(context)` afterwards to persist the changes. * * **Memory vs Storage**: Changes are immediately reflected in memory and will be * available to subsequent `get()` calls within the same context, but are not * persisted to storage until `saveChanges()` is called. * * **Value References**: The exact value reference is stored (no cloning occurs). * Ensure you don't modify objects after setting them unless you intend for those * changes to be reflected in state. * * **Type Safety**: When using TypeScript, the value must match the property's * declared type parameter. * * @example Basic usage * ```typescript * const counterProperty = userState.createProperty<number>("counter"); * * // Set a new value * await counterProperty.set(context, 42); * * // Persist to storage * await userState.saveChanges(context); * ``` * * @example Complex object * ```typescript * const userProfile = userState.createProperty<UserProfile>("profile"); * * const newProfile: UserProfile = { * name: "John Doe", * preferences: { theme: "dark", notifications: false } * }; * * await userProfile.set(context, newProfile); * await userState.saveChanges(context); * ``` * * @example Incremental updates * ```typescript * // Get current value, modify, then set * const settings = await settingsProperty.get(context, getDefaultSettings()); * settings.theme = "dark"; * settings.lastUpdated = new Date(); * * await settingsProperty.set(context, settings); * await userState.saveChanges(context); * ``` * * @example Custom key usage * ```typescript * const tenantKey = { key: `tenant_${tenantId}` }; * await dataProperty.set(context, updatedData, tenantKey); * await userState.saveChanges(context); * ``` * */ async set (context: TurnContext, value: T, customKey?: CustomKey): Promise<void> { const obj: any = await this.state.load(context, false, customKey) obj[this.name] = value } }