跳到主要内容

主线:递归事件循环与流式回合

30 秒导读: Strands 的价值核心是一个递归的事件循环。你喊一句 agent("..."),框架就反复做同一件事:让模型说一段话 → 如果模型要用工具就去执行 → 把工具结果拼回对话 → 递归再让模型说一段话……直到模型说"我说完了"(end_turn),或撞上预算上限、被取消、被中断。这一章把这个循环从对外入口一路讲到流式增量装配。

本章只讲中央循环本身。工具内部怎么跑(并发、MCP)见 02-tools;模型 provider 细节见 03-models;hooks / 中断挂点见 04-control-plane;上下文与检查点见 05-context-and-durability


1. 先建直觉:什么是"一回合"

一句话:一回合(cycle / turn)= 一次模型推理 + 紧跟其后的可选工具执行。

把 agent 想成一个只会做一件小事、但会不停重复的机器人:

  • 它把当前对话丢给模型,模型回一段话;
  • 这段话要么是"我说完了"(纯文本回答),要么是"帮我调用工具 X"(toolUse);
  • 如果是后者,机器人去把工具跑了,把结果贴回对话末尾,然后从头再来一遍

"从头再来一遍"就是递归。多轮工具调用(模型查天气 → 看到结果 → 再查航班 → 再总结)不是靠一个大 while 硬写出来的,而是靠每回合结束时递归调用自己长出来的。

一个最小的使用样子(这就是全部对外 API):

from strands import Agent

agent = Agent(tools=[get_weather]) # 装好工具
result = agent("北京今天要带伞吗?") # 一句话,内部可能跑了好几回合
print(result.stop_reason) # "end_turn"
print(result.message) # 模型的最终回答

agent("...") 背后,可能已经发生了「模型→工具→模型」两三个回合。你只看到最后的结果,但流式接口能让你看到中间每一步(见 §6)。


2. 对外入口:一道从同步到异步的阶梯

Agent 的对外入口不是一个方法,而是一层套一层的阶梯,每层只加一件事,最后落到真正的引擎 stream_async

先看这道阶梯(从上到下是调用深度,右边是每层新增的职责):

__call__(prompt) 同步入口;run_async 起一个事件循环把异步跑成同步
└ _invoke_async_and_flush 跑完 invoke_async 后,flush 记忆管理器(仅同步路径)
└ invoke_async 异步入口;消费完整个事件流,只取最后的 result
└ stream_async ★真正的引擎:并发闸门 + 逐个 yield 事件
└ _run_loop 回合的外层 while:hook 包裹 + resume 循环
└ _execute_event_loop_cycle 包一层"上下文溢出就削减重试"
└ event_loop_cycle ★中央循环(处理一回合)
└ recurse_event_loop → event_loop_cycle … 递归开下一回合

逐层职责:

符号file:line新增的那件事
同步壳Agent.__call__strands-py/src/strands/agent/agent.py:697run_async 把异步引擎跑成阻塞调用,返回 AgentResult
同步壳_invoke_async_and_flushstrands-py/src/strands/agent/agent.py:766同步路径专属:跑完再 flush() 记忆,避免关闭事件循环时丢掉后台落库
异步取结果Agent.invoke_asyncstrands-py/src/strands/agent/agent.py:779stream_async 的事件流消费光,只取最后一个事件里的 result
异步流式Agent.stream_asyncstrands-py/src/strands/agent/agent.py:1064引擎入口:并发闸门、prompt 转消息、逐个 yield 事件给调用方
回合外层Agent._run_loopstrands-py/src/strands/agent/agent.py:1220while 外循环 + Before/AfterInvocationEvent 包裹 + hook 请求 resume
溢出重试Agent._execute_event_loop_cyclestrands-py/src/strands/agent/agent.py:1318捕获 ContextWindowOverflowException,削减上下文后递归重试
中央循环event_loop_cyclestrands-py/src/strands/event_loop/event_loop.py:182处理一回合:模型推理 + 工具执行 + 递归

2.1 stream_async:引擎入口

stream_async(strands-py/src/strands/agent/agent.py:1064)是所有真活开始的地方。它做三件关键事:

  1. 并发闸门。self._concurrency.begin(idempotency_token)(agent.py:1130):同一个 agent 实例默认不允许并发调用——第二个调用要么被幂等 token 去重(等第一个的结果),要么抛 ConcurrencyException
  2. prompt → 消息。 _convert_prompt_to_messages(agent.py:1183)把字符串 / ContentBlock 列表 / Message 列表 / None 统一成 Messages
  3. 把事件流逐个吐出来。 核心是这段循环(agent.py:1193):
async for event in events: # events 来自 _run_loop
event.prepare(invocation_state=merged_state)
if event.is_callback_event: # 只有"该回调"的事件才外发
as_dict = event.as_dict()
callback_handler(**as_dict) # 驱动用户的回调
yield as_dict # 同时 yield 给 async for 的消费者

result = AgentResult(*event["stop"]) # 最后一个事件的 7-tuple 拆成结果

关键设计:事件有两类。 每个事件都是 TypedEvent,它的 is_callback_event 决定要不要外发给用户。流式增量(文本、工具入参)会外发;而 EventLoopStopEventModelStopReason 这种"内部收尾"事件 is_callback_event = False(strands-py/src/strands/types/_events.py:216:250),不外发,只在内部被拆包成结果。

2.2 invoke_async__call__:只是取结果的薄壳

invoke_async(agent.py:779)一点循环逻辑都没有——它就是把 stream_async 的流跑空,拿最后一个事件的 result:

events = self.stream_async(prompt, ...)
async for event in events:
_ = event # 丢弃中间事件
return cast(AgentResult, event["result"]) # 只要最后那个

__call__(agent.py:697)再在外面包一层 run_async(...)(agent.py:754),把整个异步过程跑成一个同步阻塞调用。所以人们平时写的 agent("..."),本质是"同步跑一遍 stream_async,只取结果"。

2.3 _run_loop:回合之外还有一层 while

容易被忽略的一点:event_loop_cycle 内部靠递归处理多回合,但它外面还套着一个 while——_run_loop(agent.py:1220)。这层 while 不是为多工具回合服务的,而是为 hook 驱动的 resume 服务的:

current_messages = messages
while current_messages is not None:
before, _ = await self.hooks.invoke_callbacks_async(BeforeInvocationEvent(...))
# ... 跑一整趟 event_loop_cycle(内部可能递归很多回合)...
after, _ = await self.hooks.invoke_callbacks_async(AfterInvocationEvent(...))
if after.resume is not None: # hook 说"接着跑"
current_messages = await self._convert_prompt_to_messages(after.resume)
else:
current_messages = None # 正常结束

所以有两层循环:内层 event_loop_cycle 的递归(一次 invocation 内的多工具回合),外层 _run_loop 的 while(一个 hook 能让整趟 invocation 再续一轮,比如 goal-loop 那种"没达标就重来")。中断(interrupt)和这层 while 的挂点细节见 04-control-plane


3. 中央循环 event_loop_cycle:一回合的解剖

这是全框架最该读透的一个函数(strands-py/src/strands/event_loop/event_loop.py:182)。它是个 async 生成器,处理一回合并把过程中的事件全 yield 出去。

怎么读下面这张图: 从上往下是一回合的执行顺序;每个 ─→ 收尾 表示这条支路直接 yield 一个终止事件并 return;只有 tool_use 那条会走到递归、开下一回合。

event_loop_cycle(一回合)

├─ _check_limits 命中预算? ─── 是 ─→ EventLoopStopEvent(limit_*) 收尾
│ 否
├─ 中断激活? 或 最新消息已含 toolUse? ─ 是 ─→ 跳过模型,直接拿这条 toolUse 去执行工具
│ 否
├─ _handle_model_execution ← 调模型,流式拼出 assistant 消息 + stop_reason
│ │
│ ├─ max_tokens ─→ 抛 MaxTokensReachedException(上层可续跑)
│ ├─ tool_use ─→ ↓(进入工具支路)
│ └─ 其它(end_turn 等) ─→ EventLoopStopEvent(stop_reason) 收尾

└─ _handle_tool_execution ← 并发跑工具,拼出 toolResult 消息

└─ recurse_event_loop ─→ 回到本图顶部,开下一回合

3.1 那个"跳过模型"的开关

图里最不显然、也最巧妙的一步在 event_loop.py:283-289:

if agent._interrupt_state.activated: # 从中断恢复
stop_reason, message = "tool_use", agent._interrupt_state.context["tool_use_message"]
elif _has_tool_use_in_latest_message(agent.messages): # 最新消息已含 toolUse
stop_reason, message = "tool_use", agent.messages[-1]
else:
... # 正常:调模型

_has_tool_use_in_latest_message(event_loop.py:99)只是检查对话最后一条消息里有没有 toolUse 块。

为什么要有这个开关? 它服务两种"继续"场景:

  • 从检查点 / 中断恢复。 上一回合模型已经产出了 toolUse,但工具还没跑(比如在 after_model 检查点被暂停,见 §7 与 05-context-and-durability.md)。恢复时重新进入循环,发现最新消息已经是待执行的 toolUse —— 于是跳过一次白花钱的模型调用,直接去执行工具。
  • 预置 toolUse。 调用方在对话里手动塞了一条 assistant toolUse 消息,想让 agent 直接执行它。

注意区分:正常多回合递归不走这条捷径。工具跑完后拼回的是一条 role: usertoolResult 消息(不含 toolUse),所以下一回合 _has_tool_use_in_latest_message 为假,模型照常被调用去解读工具结果。

3.2 支路一:_handle_model_execution(调模型)

_handle_model_execution(strands-py/src/strands/event_loop/event_loop.py:442)负责跑一次模型推理,它是个带重试循环的生成器。骨架:

while True: # 重试循环
await hooks(BeforeModelCallEvent(...)) # 模型调用前挂点(可 cancel)
async for event in middleware.invoke(InvokeModelStage, ctx, terminal):
last_event = event # 中间件链;终点是 stream_messages
yield event # 流式增量逐个外发
stop_reason, message, usage, metrics = last_event["stop"] # 4-tuple
message["metadata"] = {"usage": usage, "metrics": metrics}
await hooks(AfterModelCallEvent(...)) # 模型调用后挂点(可要求 retry)
if after_model_call_event.retry:
continue # hook 要求重试(如限流退避)
if stop_reason == "max_tokens":
message = recover_message_on_max_tokens_reached(message) # 清理半截 toolUse
break
# 循环外:把这条 assistant 消息 append 进 agent.messages,更新 usage/metrics

几个要点:

  • 真正调模型的终点在 _make_invoke_model_terminal(event_loop.py:652),它调用 stream_messages(...)(见 §5)并把流事件逐个 yield 出来。中间的 middleware.invoke 是控制面注入点,细节见 04-control-plane
  • 重试不是硬编码的,而是 hook 驱动的。 while True 只负责 continue;要不要重试由 AfterModelCallEvent.retry 标志决定,默认由 ModelRetryStrategy 这个 hook 在遇到 ModelThrottledException(限流)时设置(见 §7.2)。
  • 产出的是 ModelStopReason 事件(4-tuple:stop_reason, message, usage, metrics,_events.py:194)。回到 event_loop_cycle 后,它被过滤掉不外发(event_loop.py:295),只用来抽取 stop_reasonmessage

拿到 stop_reason 后,event_loop_cycle 分三种走(event_loop.py:304-380):

stop_reason动作file:line
max_tokensMaxTokensReachedException(半截消息已入历史,可再喊一次 agent 续跑)event_loop.py:305
tool_use进入工具支路 _handle_tool_executionevent_loop.py:316
其它(end_turn/stop_sequence…)结束本回合,yield EventLoopStopEvent 收尾event_loop.py:379

3.3 支路二:_handle_tool_execution(执行工具 + 递归)

_handle_tool_execution(strands-py/src/strands/event_loop/event_loop.py:698)是"从模型的话落到真实动作"的一端。它的主干顺序:

  1. 校验并挑出有效的 toolUse。 validate_and_prepare_tools(...)(event_loop.py:736)从 assistant 消息里抽出 toolUse 块,剔除非法的。
  2. 取消检查(执行前)。 如果 agent._cancel_signal 已被 set,给每个待执行工具补一条 "Tool execution cancelled"toolResult(保持对话合法),然后 yield EventLoopStopEvent("cancelled") 收尾(event_loop.py:750-786)。
  3. 并发执行。 交给 agent.tool_executor._execute(...)(event_loop.py:788),把每个工具的流事件转发出去。并发与实现细节见 02-tools
  4. 拼 toolResult 消息并入历史。 把所有结果打包成一条 role: user 的消息(event_loop.py:827),append 进 agent.messages,yield ToolResultMessageEvent
  5. 递归。 最后 recurse_event_loop(...)(event_loop.py:878)开下一回合。

中间还有几个"提前收尾"的岔口,顺序很重要:

  • 中断(interrupt): 工具执行里若有 ToolInterruptEvent,把中断攒起来,存进 agent._interrupt_state,yield EventLoopStopEvent("interrupt", ...) 停下等人类介入(event_loop.py:805-823)。
  • stop_event_loop / 结构化输出完成: 若请求状态里有 stop_loop 信号,直接收尾不再递归(event_loop.py:843)。
  • after_tools 检查点: 开了 checkpointing 就在这里 yield 一个 "checkpoint" 停下,持久化后再由外部恢复(event_loop.py:855,见 05-context-and-durability.md)。

4. 递归怎么长出多轮:recurse_event_loop

多轮工具对话的"引擎"就是 recurse_event_loop(strands-py/src/strands/event_loop/event_loop.py:398),它出奇地短:

async def recurse_event_loop(agent, invocation_state, ...):
recursive_trace = Trace("Recursive call", parent_id=cycle_trace.id) # 只为观测
cycle_trace.add_child(recursive_trace)
yield StartEvent()
events = event_loop_cycle(agent=agent, invocation_state=invocation_state, ...) # 再来一回合
async for event in events:
yield event
recursive_trace.end()

它就是再调一次 event_loop_cycle,并把新回合的所有事件继续 yield 上去。所以从调用栈看,多回合是 event_loop_cycle → _handle_tool_execution → recurse_event_loop → event_loop_cycle → … 一层层嵌下去,直到某回合模型返回 end_turn(或撞上预算/取消/中断),那一层不再进工具支路,yield EventLoopStopEvent 收尾,整个递归栈随之退回。

回合1: model → toolUse → 跑工具 → recurse ┐
回合2: model → toolUse → 跑工具 → recurse ┐
回合3: model → end_turn → EventLoopStopEvent(收尾,栈退回)

递归 vs. _run_loop 的 while(别混淆):

  • event_loop_cycle 的递归 = 一次 invocation 内的多工具回合(模型↔工具来回)。
  • _run_loop 的 while(§2.3)= hook 让整趟 invocation 再续一轮(粒度更粗)。

5. 流式装配:把 SSE 增量拼成一条 Message

模型 provider 吐的是增量 chunk 流(类似 SSE):一小段文本、一小段工具入参 JSON、一点 usage 元数据……streaming.py 的任务就是边收边拼,最后交出一条完整 Message + stop_reason + usage/metrics

怎么读这张图: 左边是 provider 吐出的 chunk 类型,中间是 process_stream 的状态机按类型分发,底部是拼装完成后交出的终结事件。

model.stream() ──→ chunk 增量流

process_stream 边收边更新一个 state 字典
├ messageStart → 定 role
├ contentBlockStart → 起一个 toolUse 壳(id / name)
├ contentBlockDelta → 累加 text / toolUse.input / reasoning ← handle_content_block_delta
├ contentBlockStop → 把当前块定型(工具入参在这里 json.loads)
├ messageStop → 定 stop_reason ← handle_message_stop(可纠正)
└ metadata → 抽 usage / metrics

ModelStopReason(stop_reason, message, usage, metrics) ← 交出的 4-tuple

5.1 入口 stream_messages

stream_messages(strands-py/src/strands/event_loop/streaming.py:465)是流式的入口。它做两件预处理再开流:

messages = _normalize_messages(messages) # 修空文本、修非法工具名
# 只保留 role / content 两个键,防止 metadata 等内部字段泄漏给 provider
messages = [Message(role=m["role"], content=m["content"]) for m in messages]
chunks = model.stream(messages, tool_specs or None, system_prompt, ...)
async for event in process_stream(chunks, start_time, cancel_signal):
yield event

5.2 核心 process_stream:一个小状态机

process_stream(streaming.py:394)维护一个 state 字典(累积中的 message、text、当前 tool_use、reasoning 文本等),async for 每个 chunk 按类型分发(streaming.py:443-460)。每收到一个 chunk 还会先 yield 一个原始 ModelStreamChunkEvent,再 yield 加工后的语义事件。

它也是取消的检查点之一:每轮循环开头检查 cancel_signal,若已 set 就立刻交出 stop_reason="cancelled" 并丢弃半截消息(streaming.py:426-436)。

5.3 handle_content_block_delta:增量落到哪

handle_content_block_delta(streaming.py:204)是"增量拼装"的心脏。它按 delta 的类型把内容追加到 state,并产出对应的流式事件:

delta 类型累加到产出事件
toolUsestate["current_tool_use"]["input"](字符串拼接)ToolUseStreamEvent
textstate["text"]TextStreamEvent
citationstate["citationsContent"]CitationStreamEvent
reasoningContent.textstate["reasoningText"]ReasoningTextStreamEvent

注意工具入参是按字符串增量拼的,直到 contentBlockStopjson.loads 定型(streaming.py:288);解析失败就退化成空 dict {}

5.4 handle_message_stop:纠正 provider 的"谎报"

handle_message_stop(streaming.py:335)藏着一个重要的容错:有些模型即使产出了 toolUse 也报 end_turn,这会让事件循环误以为对话结束、不去执行工具。这里强行纠正:

if stop_reason == "end_turn" and any("toolUse" in item for item in content):
stop_reason = "tool_use" # 有工具块就覆盖成 tool_use

没有这一步,§3.2 的 tool_use 支路就永远进不去 —— 这是"让工具真的能被执行"的关键补丁。


6. 停止语义:StopReason 与两种 stop 事件

一回合怎么结束,由 StopReason 说了算。它是个字面量联合类型,共 12 种(strands-py/src/strands/types/event_loop.py:39):

StopReason含义
end_turn正常说完(最常见的收尾)
tool_use模型要调工具 → 进工具支路
max_tokensprovider 的单次输出上限撞了 → 抛异常,可续跑
stop_sequence命中停止序列
limit_turns / limit_total_tokens / limit_output_tokens撞上本次调用的预算上限(见 §7)
cancelledagent.cancel() 取消
interrupt停下等人类介入
checkpoint为持久化检查点而暂停
content_filtered / guardrail_intervened内容被策略 / 护栏拦下

别把两个 stop 事件搞混,它们 tuple 长度不同:

  • ModelStopReason = 4-tuple:(stop_reason, message, usage, metrics)(strands-py/src/strands/types/_events.py:212)。这是模型层的收尾,_handle_model_execution 产出,不外发。
  • EventLoopStopEvent = 7-tuple:(stop_reason, message, metrics, request_state, interrupts, structured_output, checkpoint)(_events.py:244)。这是回合层的收尾,是整个循环交给 stream_async 的终结事件。

7-tuple 各位含义(_events.py:223-246):

位置字段说明
0stop_reason上表任一
1message最终消息
2metricsEventLoopMetrics(累计 usage/延迟/回合数)
3request_state跨回合的请求状态
4interrupts本回合抛的中断(无则 None)
5structured_output结构化输出结果(无则 None)
6checkpointstop_reason == "checkpoint" 时的检查点

stream_async 最后就是 AgentResult(*event["stop"])(agent.py:1201)把这个 7-tuple 拆成用户拿到的 AgentResult


7. 预算、限流与恢复

7.1 _check_limits:每回合开头的预算闸

_check_limits(strands-py/src/strands/event_loop/event_loop.py:63)在每回合最顶端跑一次(event_loop.py:233),对照本次调用的累计指标看有没有撞上 Limits(strands-py/src/strands/types/agent.py:17)配置的三个软上限。

同时触发时的优先级是固定的(event_loop.py:87-95):

turns > total_tokens > output_tokens

对应逻辑:

if turns_cap is not None and cycle_count >= turns_cap:
return "limit_turns"
if total_cap is not None and total_tokens >= total_cap:
return "limit_total_tokens"
if output_cap is not None and output_tokens >= output_cap:
return "limit_output_tokens"

几个要点:

  • 软上限。 检查发生在回合边界,不在模型生成中途,所以单次超大响应可能把预算超出一个回合(见 agent.py:730 的 docstring)。
  • 不抛异常,优雅收尾。 命中就 yield EventLoopStopEvent(limit_*, ...)return(event_loop.py:234-243),stop_reason 落成对应的 limit_*
  • 读的是"本次调用"的指标。latest_agent_invocation 取,复用同一个 agent 再次调用时不会被上次的计数误伤(event_loop.py:79)。

7.2 限流退避常量:MAX_ATTEMPTS / INITIAL_DELAY

event_loop.py:58-60 定了三个模块级常量:

MAX_ATTEMPTS = 6
INITIAL_DELAY = 4
MAX_DELAY = 240 # 4 分钟

它们不是被循环直接用的,而是被 agent.py:33 导入、在构造默认重试策略时喂给 ModelRetryStrategy(agent.py:437)。这个 hook 用指数退避处理 ModelThrottledException(限流):延迟 4s → 8s → 16s → 32s → 64s,封顶 240s,第 6 次仍失败就放弃并抛出(strands-py/src/strands/event_loop/_retry.py:21 的类 docstring)。

回到 §3.2:_handle_model_executionwhile True 只负责在 AfterModelCallEvent.retry 为真时 continue;"要不要重试、退避多久"完全由这个 hook 决定,循环本身对限流一无所知。这就是"控制面与主线解耦"的一个缩影(见 04-control-plane)。

7.3 取消信号与 max_tokens 恢复

取消(_cancel_signal) 是一个 threading.Event,由 agent.cancel() set,循环在多个边界协作式检查它:

检查点file:line行为
流处理每个 chunk 前streaming.py:426交出 stop_reason="cancelled",丢弃半截消息
工具执行前event_loop.py:750给每个 toolUse 补 cancel 结果,yield "cancelled" 收尾
检查点分支event_loop.py:318:855:869取消时抑制 checkpoint,改发 "cancelled" 免得多花一次模型调用

取消后 stream_asyncfinallyself._cancel_signal.clear()(agent.py:1214),让 agent 能被复用。

max_tokens 恢复: 当模型因单次输出上限被截断(stop_reason == "max_tokens"),此时的 toolUse 很可能是半截、不可靠的。_handle_model_execution 在入历史前调用 recover_message_on_max_tokens_reached(message)(event_loop.py:601)把所有 toolUse 块替换成一句说明文字(strands-py/src/strands/event_loop/_recover_message_on_max_tokens_reached.py:16),保留其它文本/图片块。这样对话仍然合法,用户再喊一次 agent 就能从干净状态续跑(随后 event_loop_cycle:305 才抛 MaxTokensReachedException)。


8. 事件的统一载体:TypedEvent(略提)

循环里 yield 的每一样东西都是 TypedEvent(strands-py/src/strands/types/_events.py:29)——一个继承自 dict 的基类。它就是个带语义外壳的字典,两个关键属性:

  • is_callback_event:是否外发给用户回调(§2.1)。流式增量为真;ModelStopReasonEventLoopStopEventToolResultEvent 为假。
  • prepare(invocation_state):外发前把 invocation 状态并进事件。

主线上会遇到的几类:StartEvent / StartEventLoopEvent(回合起点)、ModelStreamChunkEvent 及各种 *StreamEvent(流式增量)、ModelMessageEvent / ToolResultMessageEvent(消息入历史)、ModelStopReason(模型层收尾 4-tuple)、EventLoopStopEvent(回合层收尾 7-tuple)、ForceStopEvent(异常强停)。工具相关的 ToolResultEvent / ToolStreamEvent / ToolInterruptEvent02-tools


9. 代码地图(导航索引)

主题文件路径符号名
同步入口strands-py/src/strands/agent/agent.py:697Agent.__call__
异步取结果strands-py/src/strands/agent/agent.py:779Agent.invoke_async
流式引擎 / 并发闸门strands-py/src/strands/agent/agent.py:1064Agent.stream_async
回合外层 while / resumestrands-py/src/strands/agent/agent.py:1220Agent._run_loop
上下文溢出重试strands-py/src/strands/agent/agent.py:1318Agent._execute_event_loop_cycle
默认重试策略构造strands-py/src/strands/agent/agent.py:437ModelRetryStrategy(...)
中央循环(一回合)strands-py/src/strands/event_loop/event_loop.py:182event_loop_cycle
递归开下一回合strands-py/src/strands/event_loop/event_loop.py:398recurse_event_loop
模型支路(含重试循环)strands-py/src/strands/event_loop/event_loop.py:442_handle_model_execution
工具支路(含递归)strands-py/src/strands/event_loop/event_loop.py:698_handle_tool_execution
预算检查strands-py/src/strands/event_loop/event_loop.py:63_check_limits
跳过模型判定strands-py/src/strands/event_loop/event_loop.py:99_has_tool_use_in_latest_message
退避常量strands-py/src/strands/event_loop/event_loop.py:58MAX_ATTEMPTS / INITIAL_DELAY / MAX_DELAY
真正调模型的终点strands-py/src/strands/event_loop/event_loop.py:652_make_invoke_model_terminal
max_tokens 消息恢复strands-py/src/strands/event_loop/_recover_message_on_max_tokens_reached.py:16recover_message_on_max_tokens_reached
限流退避 hookstrands-py/src/strands/event_loop/_retry.py:21ModelRetryStrategy
流式入口strands-py/src/strands/event_loop/streaming.py:465stream_messages
流式状态机strands-py/src/strands/event_loop/streaming.py:394process_stream
增量落位strands-py/src/strands/event_loop/streaming.py:204handle_content_block_delta
纠正 end_turn 谎报strands-py/src/strands/event_loop/streaming.py:335handle_message_stop
StopReason 定义(12 种)strands-py/src/strands/types/event_loop.py:39StopReason
事件基类strands-py/src/strands/types/_events.py:29TypedEvent
模型层收尾 4-tuplestrands-py/src/strands/types/_events.py:194ModelStopReason
回合层收尾 7-tuplestrands-py/src/strands/types/_events.py:220EventLoopStopEvent
预算类型strands-py/src/strands/types/agent.py:17Limits