@@ -32,7 +32,24 @@ import {
3232
3333const METADATA_KEY = "tool.execute.options" ;
3434
35- export type ToolCallExecutionOptions = Omit < ToolCallOptions , "abortSignal" > ;
35+ export type ToolCallExecutionOptions = {
36+ toolCallId : string ;
37+ experimental_context ?: unknown ;
38+ /** Chat context — only present when the tool runs inside a chat.task turn. */
39+ chatId ?: string ;
40+ turn ?: number ;
41+ continuation ?: boolean ;
42+ clientData ?: unknown ;
43+ } ;
44+
45+ /** Chat context stored in locals during each chat.task turn for auto-detection. */
46+ type ChatTurnContext < TClientData = unknown > = {
47+ chatId : string ;
48+ turn : number ;
49+ continuation : boolean ;
50+ clientData ?: TClientData ;
51+ } ;
52+ const chatTurnContextKey = locals . create < ChatTurnContext > ( "chat.turnContext" ) ;
3653
3754type ToolResultContent = Array <
3855 | {
@@ -83,13 +100,33 @@ function toolFromTask<
83100 description : task . description ,
84101 inputSchema : convertTaskSchemaToToolParameters ( task ) ,
85102 execute : async ( input , options ) => {
86- const serializedOptions = options ? JSON . parse ( JSON . stringify ( options ) ) : undefined ;
103+ // Build tool metadata — skip messages (can be large) and abortSignal (non-serializable)
104+ const toolMeta : ToolCallExecutionOptions = {
105+ toolCallId : options ?. toolCallId ?? "" ,
106+ } ;
107+ if ( options ?. experimental_context !== undefined ) {
108+ try {
109+ toolMeta . experimental_context = JSON . parse ( JSON . stringify ( options . experimental_context ) ) ;
110+ } catch {
111+ // Non-serializable context — skip
112+ }
113+ }
114+
115+ // Auto-detect chat context from the parent turn
116+ const chatCtx = locals . get ( chatTurnContextKey ) ;
117+ if ( chatCtx ) {
118+ toolMeta . chatId = chatCtx . chatId ;
119+ toolMeta . turn = chatCtx . turn ;
120+ toolMeta . continuation = chatCtx . continuation ;
121+ toolMeta . clientData = chatCtx . clientData ;
122+ }
87123
88124 return await task
89125 . triggerAndWait ( input as inferSchemaIn < TTaskSchema > , {
90126 metadata : {
91- [ METADATA_KEY ] : serializedOptions ,
127+ [ METADATA_KEY ] : toolMeta as any ,
92128 } ,
129+ tags : options ?. toolCallId ? [ `toolCallId:${ options . toolCallId } ` ] : undefined ,
93130 } )
94131 . unwrap ( ) ;
95132 } ,
@@ -109,6 +146,57 @@ function getToolOptionsFromMetadata(): ToolCallExecutionOptions | undefined {
109146 return tool as ToolCallExecutionOptions ;
110147}
111148
149+ /**
150+ * Get the current tool call ID from inside a subtask invoked via `ai.tool()`.
151+ * Returns `undefined` if not running as a tool subtask.
152+ */
153+ function getToolCallId ( ) : string | undefined {
154+ return getToolOptionsFromMetadata ( ) ?. toolCallId ;
155+ }
156+
157+ /**
158+ * Get the chat context from inside a subtask invoked via `ai.tool()` within a `chat.task`.
159+ * Pass `typeof yourChatTask` as the type parameter to get typed `clientData`.
160+ * Returns `undefined` if the parent is not a chat task.
161+ *
162+ * @example
163+ * ```ts
164+ * const ctx = ai.chatContext<typeof myChat>();
165+ * // ctx?.clientData is typed based on myChat's clientDataSchema
166+ * ```
167+ */
168+ function getToolChatContext < TChatTask extends AnyTask = AnyTask > ( ) : ChatTurnContext < InferChatClientData < TChatTask > > | undefined {
169+ const opts = getToolOptionsFromMetadata ( ) ;
170+ if ( ! opts ?. chatId ) return undefined ;
171+ return {
172+ chatId : opts . chatId ,
173+ turn : opts . turn ?? 0 ,
174+ continuation : opts . continuation ?? false ,
175+ clientData : opts . clientData as InferChatClientData < TChatTask > ,
176+ } ;
177+ }
178+
179+ /**
180+ * Get the chat context from inside a subtask, throwing if not in a chat context.
181+ * Pass `typeof yourChatTask` as the type parameter to get typed `clientData`.
182+ *
183+ * @example
184+ * ```ts
185+ * const ctx = ai.chatContextOrThrow<typeof myChat>();
186+ * // ctx.chatId, ctx.clientData are guaranteed non-null
187+ * ```
188+ */
189+ function getToolChatContextOrThrow < TChatTask extends AnyTask = AnyTask > ( ) : ChatTurnContext < InferChatClientData < TChatTask > > {
190+ const ctx = getToolChatContext < TChatTask > ( ) ;
191+ if ( ! ctx ) {
192+ throw new Error (
193+ "ai.chatContextOrThrow() called outside of a chat.task context. " +
194+ "This helper can only be used inside a subtask invoked via ai.tool() from a chat.task."
195+ ) ;
196+ }
197+ return ctx ;
198+ }
199+
112200function convertTaskSchemaToToolParameters (
113201 task : AnyTask | TaskWithSchema < any , any , any >
114202) : Schema < unknown > {
@@ -136,6 +224,12 @@ function convertTaskSchemaToToolParameters(
136224export const ai = {
137225 tool : toolFromTask ,
138226 currentToolOptions : getToolOptionsFromMetadata ,
227+ /** Get the tool call ID from inside a subtask invoked via `ai.tool()`. */
228+ toolCallId : getToolCallId ,
229+ /** Get chat context (chatId, turn, clientData, etc.) from inside a subtask of a `chat.task`. Returns undefined if not in a chat context. */
230+ chatContext : getToolChatContext ,
231+ /** Get chat context or throw if not in a chat context. Pass `typeof yourChatTask` for typed clientData. */
232+ chatContextOrThrow : getToolChatContextOrThrow ,
139233} ;
140234
141235/**
@@ -756,6 +850,14 @@ function chatTask<
756850 async ( ) => {
757851 locals . set ( chatPipeCountKey , 0 ) ;
758852
853+ // Store chat context for auto-detection by ai.tool subtasks
854+ locals . set ( chatTurnContextKey , {
855+ chatId : currentWirePayload . chatId ,
856+ turn,
857+ continuation,
858+ clientData,
859+ } ) ;
860+
759861 // Per-turn stop controller (reset each turn)
760862 const stopController = new AbortController ( ) ;
761863 currentStopController = stopController ;
0 commit comments