@@ -1062,6 +1062,165 @@ function setWarmTimeoutInSeconds(seconds: number): void {
10621062 metadata . set ( WARM_TIMEOUT_METADATA_KEY , seconds ) ;
10631063}
10641064
1065+ // ---------------------------------------------------------------------------
1066+ // chat.local — per-run typed data with Proxy access
1067+ // ---------------------------------------------------------------------------
1068+
1069+ /** @internal Symbol for storing the locals key on the proxy target. */
1070+ const CHAT_LOCAL_KEY : unique symbol = Symbol ( "chatLocalKey" ) ;
1071+ /** @internal Symbol for storing the dirty-tracking locals key. */
1072+ const CHAT_LOCAL_DIRTY_KEY : unique symbol = Symbol ( "chatLocalDirtyKey" ) ;
1073+ /** @internal Counter for generating unique locals IDs. */
1074+ let chatLocalCounter = 0 ;
1075+
1076+ /**
1077+ * A Proxy-backed, run-scoped data object that appears as `T` to users.
1078+ * Includes helper methods for initialization, dirty tracking, and serialization.
1079+ * Internal metadata is stored behind Symbols and invisible to
1080+ * `Object.keys()`, `JSON.stringify()`, and spread.
1081+ */
1082+ export type ChatLocal < T extends Record < string , unknown > > = T & {
1083+ /** Initialize the local with a value. Call in `onChatStart` or `run()`. */
1084+ init ( value : T ) : void ;
1085+ /** Returns `true` if any property was set since the last check. Resets the dirty flag. */
1086+ hasChanged ( ) : boolean ;
1087+ /** Returns a plain object copy of the current value. Useful for persistence. */
1088+ get ( ) : T ;
1089+ readonly [ CHAT_LOCAL_KEY ] : ReturnType < typeof locals . create < T > > ;
1090+ readonly [ CHAT_LOCAL_DIRTY_KEY ] : ReturnType < typeof locals . create < boolean > > ;
1091+ } ;
1092+
1093+ /**
1094+ * Creates a per-run typed data object accessible from anywhere during task execution.
1095+ *
1096+ * Declare at module level, then initialize inside a lifecycle hook (e.g. `onChatStart`)
1097+ * using `chat.initLocal()`. Properties are accessible directly via the Proxy.
1098+ *
1099+ * Multiple locals can coexist — each gets its own isolated run-scoped storage.
1100+ *
1101+ * @example
1102+ * ```ts
1103+ * import { chat } from "@trigger.dev/sdk/ai";
1104+ *
1105+ * const userPrefs = chat.local<{ theme: string; language: string }>();
1106+ * const gameState = chat.local<{ score: number; streak: number }>();
1107+ *
1108+ * export const myChat = chat.task({
1109+ * id: "my-chat",
1110+ * onChatStart: async ({ clientData }) => {
1111+ * const prefs = await db.prefs.findUnique({ where: { userId: clientData.userId } });
1112+ * userPrefs.init(prefs ?? { theme: "dark", language: "en" });
1113+ * gameState.init({ score: 0, streak: 0 });
1114+ * },
1115+ * onTurnComplete: async ({ chatId }) => {
1116+ * if (gameState.hasChanged()) {
1117+ * await db.save({ where: { chatId }, data: gameState.get() });
1118+ * }
1119+ * },
1120+ * run: async ({ messages }) => {
1121+ * gameState.score++;
1122+ * return streamText({
1123+ * system: `User prefers ${userPrefs.theme} theme. Score: ${gameState.score}`,
1124+ * messages,
1125+ * });
1126+ * },
1127+ * });
1128+ * ```
1129+ */
1130+ function chatLocal < T extends Record < string , unknown > > ( ) : ChatLocal < T > {
1131+ const localKey = locals . create < T > ( `chat.local.${ chatLocalCounter ++ } ` ) ;
1132+ const dirtyKey = locals . create < boolean > ( `chat.local.${ chatLocalCounter ++ } .dirty` ) ;
1133+
1134+ const target = { } as any ;
1135+ target [ CHAT_LOCAL_KEY ] = localKey ;
1136+ target [ CHAT_LOCAL_DIRTY_KEY ] = dirtyKey ;
1137+
1138+ return new Proxy ( target , {
1139+ get ( _target , prop , _receiver ) {
1140+ // Internal Symbol properties
1141+ if ( prop === CHAT_LOCAL_KEY ) return _target [ CHAT_LOCAL_KEY ] ;
1142+ if ( prop === CHAT_LOCAL_DIRTY_KEY ) return _target [ CHAT_LOCAL_DIRTY_KEY ] ;
1143+
1144+ // Instance methods
1145+ if ( prop === "init" ) {
1146+ return ( value : T ) => {
1147+ locals . set ( localKey , value ) ;
1148+ locals . set ( dirtyKey , false ) ;
1149+ } ;
1150+ }
1151+ if ( prop === "hasChanged" ) {
1152+ return ( ) => {
1153+ const dirty = locals . get ( dirtyKey ) ?? false ;
1154+ locals . set ( dirtyKey , false ) ;
1155+ return dirty ;
1156+ } ;
1157+ }
1158+ if ( prop === "get" ) {
1159+ return ( ) => {
1160+ const current = locals . get ( localKey ) ;
1161+ if ( current === undefined ) {
1162+ throw new Error (
1163+ "local.get() called before initialization. Call local.init() first."
1164+ ) ;
1165+ }
1166+ return { ...current } ;
1167+ } ;
1168+ }
1169+ // toJSON for serialization (JSON.stringify(local))
1170+ if ( prop === "toJSON" ) {
1171+ return ( ) => {
1172+ const current = locals . get ( localKey ) ;
1173+ return current ? { ...current } : undefined ;
1174+ } ;
1175+ }
1176+
1177+ const current = locals . get ( localKey ) ;
1178+ if ( current === undefined ) return undefined ;
1179+ return ( current as any ) [ prop ] ;
1180+ } ,
1181+
1182+ set ( _target , prop , value ) {
1183+ // Don't allow setting internal Symbols
1184+ if ( typeof prop === "symbol" ) return false ;
1185+
1186+ const current = locals . get ( localKey ) ;
1187+ if ( current === undefined ) {
1188+ throw new Error (
1189+ "chat.local can only be modified after initialization. " +
1190+ "Call local.init() in onChatStart or run() first."
1191+ ) ;
1192+ }
1193+ locals . set ( localKey , { ...current , [ prop ] : value } ) ;
1194+ locals . set ( dirtyKey , true ) ;
1195+ return true ;
1196+ } ,
1197+
1198+ has ( _target , prop ) {
1199+ if ( typeof prop === "symbol" ) return prop in _target ;
1200+ const current = locals . get ( localKey ) ;
1201+ return current !== undefined && prop in current ;
1202+ } ,
1203+
1204+ ownKeys ( ) {
1205+ const current = locals . get ( localKey ) ;
1206+ return current ? Reflect . ownKeys ( current ) : [ ] ;
1207+ } ,
1208+
1209+ getOwnPropertyDescriptor ( _target , prop ) {
1210+ if ( typeof prop === "symbol" ) return undefined ;
1211+ const current = locals . get ( localKey ) ;
1212+ if ( current === undefined || ! ( prop in current ) ) return undefined ;
1213+ return {
1214+ configurable : true ,
1215+ enumerable : true ,
1216+ writable : true ,
1217+ value : ( current as any ) [ prop ] ,
1218+ } ;
1219+ } ,
1220+ } ) as ChatLocal < T > ;
1221+ }
1222+
1223+
10651224/**
10661225 * Extracts the client data (metadata) type from a chat task.
10671226 * Use this to type the `metadata` option on the transport.
@@ -1088,6 +1247,8 @@ export const chat = {
10881247 task : chatTask ,
10891248 /** Pipe a stream to the chat transport. See {@link pipeChat}. */
10901249 pipe : pipeChat ,
1250+ /** Create a per-run typed local. See {@link chatLocal}. */
1251+ local : chatLocal ,
10911252 /** Create a public access token for a chat task. See {@link createChatAccessToken}. */
10921253 createAccessToken : createChatAccessToken ,
10931254 /** Override the turn timeout at runtime (duration string). See {@link setTurnTimeout}. */
0 commit comments