@@ -36,6 +36,7 @@ export class Agent {
3636 private abortController ?: AbortController ;
3737 private running ?: Promise < void > ;
3838 private runChannel ?: { push : RunChannelPush } ;
39+ private outstandingHandle ?: AgentRunHandle ;
3940
4041 private _state : AgentState ;
4142
@@ -103,41 +104,51 @@ export class Agent {
103104
104105 abort ( ) : void {
105106 this . abortController ?. abort ( ) ;
107+ this . outstandingHandle = undefined ;
106108 }
107109
108110 waitForIdle ( ) : Promise < void > {
109111 return this . running ?? Promise . resolve ( ) ;
110112 }
111113
112114 prompt ( input : string | Message , opts ?: { maxSteps ?: number } ) : AgentRunHandle {
113- if ( this . _state . isStreaming ) {
114- throw new Error ( 'Agent is already processing a prompt' ) ;
115- }
115+ this . assertIdle ( 'prompt' ) ;
116116
117117 const message = typeof input === 'string' ? createUserMessage ( input ) : input ;
118118 this . _state . stepCount = 0 ;
119119
120- return new DefaultAgentRunHandle ( async ( push , signal ) =>
121- this . runLoop ( {
120+ const handle : AgentRunHandle = new DefaultAgentRunHandle ( async ( push , signal ) => {
121+ if ( this . outstandingHandle === handle ) {
122+ this . outstandingHandle = undefined ;
123+ }
124+ return this . runLoop ( {
122125 initialMessages : [ message ] ,
123126 externalPush : push ?? undefined ,
124127 externalAbortSignal : signal ,
125128 maxSteps : opts ?. maxSteps ?? this . defaultMaxSteps ,
126- } )
127- ) ;
129+ } ) ;
130+ } ) ;
131+ this . outstandingHandle = handle ;
132+ return handle ;
128133 }
129134
130135 continue ( opts ?: { maxSteps ?: number } ) : AgentRunHandle {
131- if ( this . _state . isStreaming ) {
132- throw new Error ( 'Agent is already processing' ) ;
133- }
136+ this . assertIdle ( 'continue' ) ;
134137
135138 if ( this . _state . messages . length === 0 ) {
136139 throw new Error ( 'No messages to continue from' ) ;
137140 }
138141
139142 const pendingMessage = this . findMostRecentPendingAssistant ( ) ;
140143 if ( pendingMessage ) {
144+ const pendingIndex = this . _state . messages . indexOf ( pendingMessage ) ;
145+ const trailing = this . _state . messages . slice ( pendingIndex + 1 ) ;
146+ const hasNonToolResultTrailing = trailing . some ( ( m ) => m . role !== 'toolResult' ) ;
147+ if ( hasNonToolResultTrailing ) {
148+ throw new Error (
149+ 'Cannot continue() with a pending decision when non-toolResult messages have been appended after the pending assistant. Use injectDeferralResults() + prompt() instead — see the agentic-kit deferral docs.'
150+ ) ;
151+ }
141152 const pendingDecisions = this . findPendingDecisions ( pendingMessage ) ;
142153 for ( const { tool, decision } of pendingDecisions ) {
143154 const errors = validateSchema ( tool . decision ! , decision , 'root' ) ;
@@ -154,13 +165,29 @@ export class Agent {
154165 }
155166 }
156167
157- return new DefaultAgentRunHandle ( async ( push , signal ) =>
158- this . runLoop ( {
168+ const handle : AgentRunHandle = new DefaultAgentRunHandle ( async ( push , signal ) => {
169+ if ( this . outstandingHandle === handle ) {
170+ this . outstandingHandle = undefined ;
171+ }
172+ return this . runLoop ( {
159173 externalPush : push ?? undefined ,
160174 externalAbortSignal : signal ,
161175 maxSteps : opts ?. maxSteps ?? this . defaultMaxSteps ,
162- } )
163- ) ;
176+ } ) ;
177+ } ) ;
178+ this . outstandingHandle = handle ;
179+ return handle ;
180+ }
181+
182+ private assertIdle ( method : 'prompt' | 'continue' ) : void {
183+ if ( this . _state . isStreaming ) {
184+ throw new Error ( `Agent is already processing; cannot call ${ method } () while a run is active` ) ;
185+ }
186+ if ( this . outstandingHandle ) {
187+ throw new Error (
188+ `Agent has an unconsumed run handle from a previous ${ method } ()/prompt()/continue() call; consume it (events / toReadableStream / toResponse / wait) or abort the agent before issuing another`
189+ ) ;
190+ }
164191 }
165192
166193 private findMostRecentPendingAssistant ( ) : AssistantMessage | undefined {
@@ -227,7 +254,7 @@ export class Agent {
227254 }
228255 }
229256
230- let stopReason : 'completed' | 'max_steps' = 'completed' ;
257+ let stopReason : 'completed' | 'max_steps' | 'aborted' = 'completed' ;
231258
232259 try {
233260 await this . emit ( { type : 'agent_start' } ) ;
@@ -286,6 +313,11 @@ export class Agent {
286313 }
287314
288315 await this . emit ( { type : 'turn_end' , message : assistantMessage , toolResults : outcome . results } ) ;
316+
317+ if ( localAbortController . signal . aborted ) {
318+ stopReason = 'aborted' ;
319+ break ;
320+ }
289321 }
290322
291323 await this . emit ( { type : 'agent_end' , messages : [ ...this . _state . messages ] , stopReason } ) ;
@@ -372,6 +404,10 @@ export class Agent {
372404 continue ;
373405 }
374406
407+ if ( signal . aborted ) {
408+ break ;
409+ }
410+
375411 const tool = this . _state . tools . find ( ( candidate ) => candidate . name === toolCall . name ) ;
376412 const args = toolCall . arguments as Record < string , unknown > ;
377413 const decisionAttached = 'decision' in toolCall && toolCall . decision !== undefined ;
0 commit comments