@@ -22,7 +22,6 @@ import {
22
22
runWithContext ,
23
23
runWithStreamingCallback ,
24
24
sentinelNoopStreamingCallback ,
25
- stripUndefinedProps ,
26
25
z ,
27
26
} from '@genkit-ai/core' ;
28
27
import { Channel } from '@genkit-ai/core/async' ;
@@ -46,6 +45,7 @@ import {
46
45
ModelArgument ,
47
46
ModelMiddleware ,
48
47
Part ,
48
+ ToolRequestPart ,
49
49
ToolResponsePart ,
50
50
resolveModel ,
51
51
} from './model.js' ;
@@ -75,7 +75,16 @@ export interface ResumeOptions {
75
75
* Tools have a `.reply` helper method to construct a reply ToolResponse and validate
76
76
* the data against its schema. Call `myTool.reply(interruptToolRequest, yourReplyData)`.
77
77
*/
78
- reply : ToolResponsePart | ToolResponsePart [ ] ;
78
+ reply ?: ToolResponsePart | ToolResponsePart [ ] ;
79
+ /**
80
+ * restart will run a tool again with additionally supplied metadata passed through as
81
+ * a `resumed` option in the second argument. This allows for scenarios like conditionally
82
+ * requesting confirmation of an LLM's tool request.
83
+ *
84
+ * Tools have a `.restart` helper method to construct a restart ToolRequest. Call
85
+ * `myTool.restart(interruptToolRequest, resumeMetadata)`.
86
+ */
87
+ restart ?: ToolRequestPart | ToolRequestPart [ ] ;
79
88
/** Additional metadata to annotate the created tool message with in the "resume" key. */
80
89
metadata ?: Record < string , any > ;
81
90
}
@@ -141,53 +150,6 @@ export interface GenerateOptions<
141
150
context ?: ActionContext ;
142
151
}
143
152
144
- /** Amends message history to handle `resume` arguments. Returns the amended history. */
145
- async function applyResumeOption (
146
- options : GenerateOptions ,
147
- messages : MessageData [ ]
148
- ) : Promise < MessageData [ ] > {
149
- if ( ! options . resume ) return messages ;
150
- if (
151
- messages . at ( - 1 ) ?. role !== 'model' ||
152
- ! messages
153
- . at ( - 1 )
154
- ?. content . find ( ( p ) => p . toolRequest && p . metadata ?. interrupt )
155
- ) {
156
- throw new GenkitError ( {
157
- status : 'FAILED_PRECONDITION' ,
158
- message : `Cannot 'resume' generation unless the previous message is a model message with at least one interrupt.` ,
159
- } ) ;
160
- }
161
- const lastModelMessage = messages . at ( - 1 ) ! ;
162
- const toolRequests = lastModelMessage . content . filter ( ( p ) => ! ! p . toolRequest ) ;
163
-
164
- const pendingResponses : ToolResponsePart [ ] = toolRequests
165
- . filter ( ( t ) => ! ! t . metadata ?. pendingOutput )
166
- . map ( ( t ) =>
167
- stripUndefinedProps ( {
168
- toolResponse : {
169
- name : t . toolRequest ! . name ,
170
- ref : t . toolRequest ! . ref ,
171
- output : t . metadata ! . pendingOutput ,
172
- } ,
173
- metadata : { source : 'pending' } ,
174
- } )
175
- ) as ToolResponsePart [ ] ;
176
-
177
- const reply = Array . isArray ( options . resume . reply )
178
- ? options . resume . reply
179
- : [ options . resume . reply ] ;
180
-
181
- const message : MessageData = {
182
- role : 'tool' ,
183
- content : [ ...pendingResponses , ...reply ] ,
184
- metadata : {
185
- resume : options . resume . metadata || true ,
186
- } ,
187
- } ;
188
- return [ ...messages , message ] ;
189
- }
190
-
191
153
export async function toGenerateRequest (
192
154
registry : Registry ,
193
155
options : GenerateOptions
@@ -202,8 +164,6 @@ export async function toGenerateRequest(
202
164
if ( options . messages ) {
203
165
messages . push ( ...options . messages . map ( ( m ) => Message . parseData ( m ) ) ) ;
204
166
}
205
- // resuming from interrupts occurs after message history but before user prompt
206
- messages = await applyResumeOption ( options , messages ) ;
207
167
if ( options . prompt ) {
208
168
messages . push ( {
209
169
role : 'user' ,
@@ -216,6 +176,19 @@ export async function toGenerateRequest(
216
176
message : 'at least one message is required in generate request' ,
217
177
} ) ;
218
178
}
179
+ if (
180
+ options . resume &&
181
+ ! (
182
+ messages . at ( - 1 ) ?. role === 'model' &&
183
+ messages . at ( - 1 ) ?. content . find ( ( p ) => ! ! p . toolRequest )
184
+ )
185
+ ) {
186
+ throw new GenkitError ( {
187
+ status : 'FAILED_PRECONDITION' ,
188
+ message : `Last message must be a 'model' role with at least one tool request to 'resume' generation.` ,
189
+ detail : messages . at ( - 1 ) ,
190
+ } ) ;
191
+ }
219
192
let tools : Action < any , any > [ ] | undefined ;
220
193
if ( options . tools ) {
221
194
tools = await resolveTools ( registry , options . tools ) ;
@@ -386,6 +359,12 @@ export async function generate<
386
359
format : resolvedOptions . output . format ,
387
360
jsonSchema : resolvedSchema ,
388
361
} ,
362
+ // coerce reply and restart into arrays for the action schema
363
+ resume : resolvedOptions . resume && {
364
+ reply : [ resolvedOptions . resume . reply || [ ] ] . flat ( ) ,
365
+ restart : [ resolvedOptions . resume . restart || [ ] ] . flat ( ) ,
366
+ metadata : resolvedOptions . resume . metadata ,
367
+ } ,
389
368
returnToolRequests : resolvedOptions . returnToolRequests ,
390
369
maxTurns : resolvedOptions . maxTurns ,
391
370
} ;
0 commit comments