@@ -18,8 +18,12 @@ import { logger } from '../logger'
1818// Agent 管 context(messages/system/options),tool-use 循环借力 SDK
1919// (循环在 LLMClient.stream 内由 stop_reason 驱动,这里只编排多轮)。
2020// maxTokens 从 config.defaultOptions 取(铁律8)。
21+ //
22+ // maxIterations 是防死循环保险丝,不是「任务做完」信号:
23+ // 触顶且末轮仍是 tool_use 时,强制再打一轮无工具收尾,避免半截话当终局。
2124
22- const DEFAULT_MAX_ITERATIONS = 10
25+ /** 工具循环默认上限(原 10 对多步 shell/检索任务偏紧) */
26+ export const DEFAULT_MAX_ITERATIONS = 32
2327
2428/**
2529 * 运行时上下文注入(system 末尾):当前本地时间 + 时区。
@@ -60,21 +64,29 @@ export class Agent {
6064 * 1. 组装 messages + system + tools + maxTokens(从 defaultOptions)
6165 * 2. stream LLM,逐 delta 回调
6266 * 3. 若 stop_reason='tool_use' → 执行工具 → 追加 tool_result → 继续循环
63- * 4. 直至 stop_reason 非 tool_use 或达上限
67+ * 4. 直至 stop_reason 非 tool_use;若触顶仍停在 tool_result 后 → 强制无工具收尾轮
6468 */
6569 async run (
6670 input : AgentRunInput ,
6771 callbacks : AgentRunCallbacks = { } ,
6872 limits : AgentLimits = { } ,
69- ) : Promise < { messages : LlmMessage [ ] ; finalText : string ; finalThinking : string } > {
73+ ) : Promise < {
74+ messages : LlmMessage [ ]
75+ finalText : string
76+ finalThinking : string
77+ /** 是否因 maxIterations 触顶而强制收尾(非正常 end_turn) */
78+ hitIterationLimit : boolean
79+ } > {
7080 const maxIter = limits . maxIterations ?? DEFAULT_MAX_ITERATIONS
7181 let functionCallCount = 0
7282 const messages = [ ...input . messages ]
7383 const tools = this . resolveTools ( )
7484 const client = getClient ( this . config . modelId , this . deps . llmOpts )
85+ const system = injectRuntimeContext ( this . config . instructions )
7586
7687 let finalText = ''
7788 let finalThinking = ''
89+ let hitIterationLimit = false
7890
7991 for ( let iter = 0 ; iter < maxIter ; iter ++ ) {
8092 if ( input . signal ?. aborted ) {
@@ -84,7 +96,7 @@ export class Agent {
8496 logger . debug ( '[agent] thinking config:' , this . config . thinking )
8597 const response = await client . stream ( {
8698 model : this . config . modelId ,
87- system : injectRuntimeContext ( this . config . instructions ) ,
99+ system,
88100 messages,
89101 tools : tools . length ? tools : undefined ,
90102 maxTokens : this . config . defaultOptions . maxTokens , // 铁律8
@@ -166,9 +178,38 @@ export class Agent {
166178 finalText = JSON . stringify ( { handoff_to : handoffTarget } )
167179 break
168180 }
181+
182+ // 本轮是最后一轮迭代槽且刚执行完工具 → 循环将结束,标记需收尾
183+ if ( iter === maxIter - 1 ) {
184+ hitIterationLimit = true
185+ }
186+ }
187+
188+ // 触顶停在 tool_result 之后:强制无工具收尾,让模型基于已有结果给最终答复
189+ if ( hitIterationLimit && needsToolResultFinalization ( messages ) && ! input . signal ?. aborted ) {
190+ logger . warn (
191+ `[agent:${ this . config . name } ] 达 maxIterations=${ maxIter } ,强制无工具收尾轮` ,
192+ )
193+ const response = await client . stream ( {
194+ model : this . config . modelId ,
195+ system,
196+ messages,
197+ tools : undefined , // 禁止再调工具
198+ maxTokens : this . config . defaultOptions . maxTokens ,
199+ temperature : this . config . defaultOptions . temperature ,
200+ thinking : this . config . thinking ,
201+ signal : input . signal ,
202+ onDelta : ( delta : LlmDelta ) => this . emitDelta ( delta , callbacks ) ,
203+ onRetry : ( info ) => callbacks . onRetry ?.( info ) ,
204+ } )
205+ messages . push ( { role : 'assistant' , content : response . content } )
206+ const text = extractText ( response . content )
207+ if ( text ) finalText = text
208+ const thinkingText = extractThinking ( response . content )
209+ if ( thinkingText ) finalThinking = thinkingText
169210 }
170211
171- return { messages, finalText, finalThinking }
212+ return { messages, finalText, finalThinking, hitIterationLimit }
172213 }
173214
174215 private resolveTools ( ) {
@@ -196,4 +237,12 @@ function extractThinking(blocks: LlmContentBlock[]): string {
196237 . map ( ( b ) => b . thinking )
197238 . join ( '' )
198239}
240+
241+ /** 末条是否为 tool_result user 消息(触顶后尚无最终 assistant 答复) */
242+ function needsToolResultFinalization ( messages : LlmMessage [ ] ) : boolean {
243+ const last = messages [ messages . length - 1 ]
244+ if ( ! last || last . role !== 'user' || typeof last . content === 'string' ) return false
245+ return last . content . some ( ( b ) => b . type === 'tool_result' )
246+ }
247+
199248export type { LlmResponse }
0 commit comments