人工智能大模型AI AgentAgent 框架多智能体工具调用MCP 服务【免费下载链接】harness-sdkBuild an agent harness and control it end-to-end. Open-source SDK for production AI agents in Python TypeScript - any model, any cloud.项目地址https://gitcode.com/GitHub_Trending/sdkpython13/harness-sdk点击查看免费下载导读本文以 strands-ts/src/middleware/README.md 为核心系统讲解 strands SDKTypeScript / Python 双语言实现中中间件Middleware系统的设计决策、阶段Stage模型、执行顺序与行为契约。中间件是 SDK 中用于在模型调用、工具执行和整个 Agent 输出流三个关键拦截点上进行拦截、转换与短路的核心扩展机制也是 SDK 长期规划中取代 Hooks 的统一扩展模型。读完本文你将掌握三个内建 Stage 的语义差异、Input / Wrap / Output 三相执行顺序、Hooks 与中间件的边界划分以及如何基于 MiddlewareRegistry 编写可复用的缓存、限流、重试与 Mock 中间件。一、设计定位中间件与 Hooks 的关系1.1 Hooks 永远在中间件链之外触发中间件系统有一条贯穿始终的核心设计决策Hooks 永远在中间件链之外触发。这意味着两件事即使中间件短路short-circuitHooks 依然触发。如果某个中间件在未调用next()的情况下直接返回缓存结果该阶段对应的 Before/After Hook 对依然会执行。中间件的内部重试对 Hooks 不可见。如果中间件多次调用next()例如针对限流错误进行重试Hooks 只会看到一次调用——而不是三次。内部重试属于实现细节应通过中间件本身来观测而不是依赖 Hook 计数。这一决策在测试中得到了明确验证。agent-middleware.test.ts 中Before/After hooks fire even when middleware short-circuits用例证实中间件返回合成结果不调用模型时BeforeModelCallEvent与AfterModelCallEvent仍然触发。1.2 中间件长期取代 Hooks中间件的定位是长期取代 HooksInput/Output 两相已经覆盖了 Before/After Hook 对的使用场景但处于一个统一模型之内。对于没有 before/after 配对的一次性事件例如MessageAdded迁移路径尚不明确——但总体目标是在可行处统一收敛到中间件。1.3 实践后果限流中间件短路时监控 Hook 仍会看到调用——消费者监控调用量时会看到尝试即使模型从未被真正调用重试中间件调用next()三次只产生一对 Hook——如果消费者需要按尝试次数的可观测性应在同一阶段注册中间件而非 HooksResume 逻辑用不同参数调用next()对 Hooks 天然不可见——Hooks 只看到最外层的调用/结果边界。二、执行模型阶段与相位2.1 执行流程总览原文档给出的执行模型如下BeforeHookEvent ← always fires → Input phase ← transforms context → Output phase ← wraps Wrap, transforms result on the way out → Wrap phase ← full wrap, may retry/short-circuit → terminal ← actual operation AfterHookEvent ← always fires (even on short-circuit)2.2 排序规则先注册者在外先注册的处理器位于最外层outermost。在每个相位内处理器按注册顺序执行。相位顺序是固定的所有 Input → 所有 Output → 所有 Wrap → terminal。相位机制部分解决了排序问题——它给每个位置赋予了具体语义转换输入、转换输出、包裹执行。但由于中间件由插件贡献插件注册顺序在同一相位内仍然重要。2.3 排序问题的长期方案如果插件排序将来成为痛点文档列出了两条候选方案在addMiddleware上增加显式的 priority/order 字段实现细粒度控制SDK 内部使用不向客户暴露的内部相位例如 retry 相位。两条路径都有效最终选择取决于是否重访内建 SDK 功能构建在插件之上这一模式。2.4 函数式风格优先于原地变更中间件围绕向前传递转换后的值而非原地变更共享状态来设计Input 处理器返回一个新的 contextOutput 处理器返回一个新的 resultWrap 处理器向next()传递一个可能被修改的context。这与 Hooks 原地修改事件属性的风格形成对比。不过 SDK 并不阻止变更——context 上的agent引用是面向高级用例的逃生舱——但 API 的设计倾向于函数式风格每一层向下游传递的效果都是显式的。三、注册与组合MiddlewareRegistry 源码解析3.1 注册 API 与阶段令牌TypeScript 的Agent.addMiddleware提供四个重载分别对应阶段令牌与三个相位子令牌见 agent.ts// Input 相位执行前转换 context agent.addMiddleware(InvokeModelStage.Input, async (context) ({ ...context, systemPrompt: injectToSystemPrompt(context), })) // Wrap 相位完整包裹等价于直接传阶段令牌 agent.addMiddleware(InvokeModelStage, async function* (context, next) { const start Date.now() const result yield* next(context) console.log(Model call took ${Date.now() - start}ms) return result }) // Output 相位执行完成后转换结果 agent.addMiddleware(InvokeModelStage.Output, async (result) { log(Model returned stopReason${result.result.stopReason}) return result })addMiddleware的每个重载都返回一个清理函数调用后通过MiddlewareRegistry.remove摘除该处理器。3.2 相位组合的源码实现registry.ts 中的PHASE_ORDER常量定义了相位排序{ input: 0, output: 1, wrap: 2 }。compose()先按相位稳定排序再反向构建链最终形成的组合层次是Compose layering: input (outermost) → output → wrap (innermost) Execution order: input → wrap → outputaddInput将同步/异步的MiddlewareInputHandler适配为 async generator先执行handler(context)得到转换后的 context再yield* next(transformed)。addOutput则先yield* next(context)得到结果再执行handler(result)转换。两个适配器都返回适配后的处理器引用供remove()精确摘除。3.3 阶段令牌createStage 与类型推断stages.ts 中的createStage创建冻结的MiddlewareStage对象携带Input/Wrap/Output三个冻结的相位子令牌并被 registry 用作 Map 键。阶段令牌通过泛型携带 Context/Event/Result 类型实现注册处的完整类型推断。types.ts额外导出MiddlewareHandlerOfS与MiddlewareNextOfS工具类型可从阶段令牌提取对应的处理器与next签名。四、模型调用阶段InvokeModelStage4.1 Context 字段语义InvokeModelContext包含agent逃生舱、model本次调用的模型、messages只读消息数组、systemPrompt、toolSpecs、toolChoice、invocationState共享引用、projectedInputTokens预估输入 token 数以及dynamicTrailingBlocks缓存点相关的尾部动态块数。其中集合字段是防御性拷贝invocationState与model是共享引用。4.2 Per-call Model单次调用的模型替换InvokeModelContext.model是 terminal 实际调用的模型初始化为agent.model。中间件可以在不改变 agent 状态的前提下为单次调用替换模型且被选中的模型决定 trace span 上记录的 model IDconst modified { ...context, model: otherModel }agent-middleware.test.ts 中middleware can select the model for one call用例验证了这一点Input 处理器替换模型后默认模型的stream从未被调用agent 返回了替换模型的响应。4.3 结果转换的权威性Output 中间件可以转换结果stopReason、message、usage、metrics。转换后的结果是权威的——它既是AgentResult返回给调用方的结果也是被追加到agent.messages的结果。一个将stopReason从tool_use改为end_turn的中间件可以阻止工具调度。4.4 modelState 隔离中间件不可访问或修改模型状态modelState被有意排除在InvokeModelContext之外。agent 在中间件链运行前对模型状态做快照并在整条链完成后将模型 provider 的变更写回。中间件对agent.modelState的任何变更——无论发生在next()之前还是之后——都会被这次写回覆盖。copy-on-input.test.ts 中modelState is not exposed on the middleware context、model state changes are written back to agent.modelState after streaming与middleware mutations to agent.modelState before next() do not affect the model call三组用例完整覆盖了这一契约provider 在流式期间的写入和删除都会同步回agent.modelState而中间件的写入无效。4.5 典型用例瞬态错误重试中间件捕获错误后再次调用next插件注册插件通过initAgent/init_agent注册中间件。仓库中的 ContextInjector 插件 即是在InvokeModelStage.Input上注册注入中间件将即时渲染的上下文文本折入每次模型调用而不触碰持久历史。五、工具执行阶段ExecuteToolStage5.1 Context 字段与工具转换ExecuteToolContext包含agent、tool解析后的工具实现未找到时为undefined、toolUse名称、toolUseId、input、invocationState共享引用与cancelSignal执行器拥有的取消信号中间件可观察但不可替换。中间件可以通过修改 context 中的toolUse.input让工具收到修改后的参数上下文修改不会变更原始 context 对象。测试modified input reaches the tool验证了转换后的入参确实到达工具函数。5.2 短路与 Mock中间件可以不调用next()直接返回一个合成的ExecuteToolResult来实现工具 Mock// eslint-disable-next-line require-yield const middleware: ExecuteToolMiddleware async function* (context) { return { result: new ToolResultBlock({ toolUseId: context.toolUse.toolUseId, status: success, content: [new TextBlock(mocked result)], }), } }此时真实工具函数不会被调用短路结果进入会话AfterToolCallEvent.result在短路时包含中间件提供的结果。缓存插件即是此模式首次调用执行第二次相同输入直接返回缓存结果。5.3 Hooks 边界BeforeToolCallEvent在中间件执行前触发AfterToolCallEvent在中间件完成后触发即使短路两个 Hook 依然触发。六、Agent 输出流阶段AgentStreamStage内部6.1 为什么是内部 APIAgentStreamStage不对外导出index.ts 仅导出InvokeModelStage与ExecuteToolStage两个公开阶段。该阶段的 context 按引用传递args与options但正确的契约拷贝 vs. 引用、readonly 强制尚未定稿。为避免发布不一致的表面SDK 在决定args是应被深拷贝与InvokeModelStage.messages保持一致还是保持引用更简单但与其他阶段的保证相比令人意外之前一直将其保持为内部阶段。6.2 能力过滤、注入与短路事件流虽然阶段内部但它的能力在测试中得到了充分验证agent-middleware.test.ts过滤事件手动迭代next(context)生成器仅 yield 匹配谓词的事件注入事件在内部链事件之前、之后或穿插处 yield 合成事件甚至可产出纯合成事件流而不调用next()短路整个流直接返回合成AgentResult模型不被调用但BeforeInvocationEvent/AfterInvocationEvent依然在链外触发。6.3 invocationState 按引用共享invocationState不会被拷贝。工具和 Hooks 写入它这些变更必须出现在AgentResult.invocationState上。SDK 绝不应直接写入它——键空间属于调用方。七、中间件发起的 Interrupt人工介入7.1 ExecuteToolStage 中断调用context.interrupt(name)见 interrupt.ts 中的createMiddlewareInterrupt实现时无既有响应时抛出InterruptError并中止 agent恢复后用户提供响应返回包装在MiddlewareInterruptResult中的响应为前向兼容而包装提供预置响应参数则完全跳过中断中间件中断时工具不执行中断 ID 包含工具使用 ID确定且作用域限定中断来源为middleware区别于 hook/tool 中断中断注册在 agent 的中断状态中拷贝/展开 context 会保留interrupt()函数。createMiddlewareInterrupt的实现要点是中断 ID 由${idPrefix}:${params.name}构成优先解析priorResponses快照中的既有响应其次使用params.response预置响应否则构造Interrupt并抛出InterruptError。它是只读的——只检查既有响应从不自行注册中断注册点是执行器的InterruptException处理器。7.2 AgentStreamStage 中断无既有响应时抛出/引发并中止 agent恢复后返回响应中断 ID 使用agentStream命名空间中断事件在流上产出恢复到非工具完成态后中断状态被清除下一次全新调用可被接受恢复不会破坏挂起的工具中断若工具中断未决且其恢复在工具运行前被取消该中断会存续并保持可恢复stop 报告所有仍未回答的中断而不仅是刚引发的一个使部分回答的集合对调用方完全可见。7.3 中间件不能恢复中断AgentStreamStage中间件当前无法恢复工具级中断。中断解析_interruptState.resume()运行在stream()的外层循环中、位于中间件链之外。当工具级中断触发时_stream在内部捕获InterruptError并返回正常的AgentResultstopReason: interrupt——中间件能看到结果但无法携带中断响应重新进入流。要在单次调用内以编程方式恢复中断应在 Hook 中使用AfterInvocationEvent.resume。未来的增强可能在AgentStreamResult或AgentStreamContext上增加恢复机制让中间件可以携带中断响应重新进入。八、遥测记录中间件处理后的状态Trace span 记录的是中间件转换之后的数据而非原始未经中间件处理的输入。例如模型调用 span 记录的是模型实际收到的 messages 与 system prompt反映InvokeModelStage中间件施加的任何转换。这是刻意的设计span 应反映实际发生的情况而不是中间件介入前被请求的内容。agent.tracer.test.node.ts 中的model span records post-middleware context when InvokeModelStage middleware transforms input用例直接验证了此行为。九、行为需求清单跨语言契约原文档将下述需求标记为由测试验证、应在各语言实现中保持一致的行为契约。整理如下9.1 注册组合Registry Composition无处理器快速路径阶段未注册任何处理器时terminal 直接运行事件与结果原样通过Wrap 处理器透传处理器原样转发所有事件与结果处理器可在调用next前修改 contextterminal 收到修改后的 context多个处理器各自修改 context 会链式累积处理器可不调用next直接产出结果短路terminal 永不被调用处理器可转换结果可过滤事件只 yield 匹配谓词的事件可在内部链事件前后注入事件可通过多次调用next重试如瞬态错误组合顺序先注册者最外层进入时先执行、退出时最后执行多个处理器都能观察到流经链的事件Input 相位在 Wrap 链运行前转换 context支持同步/异步无论注册顺序如何Input 都先于 Wrap 运行多个 Input 处理器按注册顺序组合Output 相位在 Wrap 链完成后转换结果支持同步/异步不影响流式事件只影响结果无论注册顺序如何Output 都后于 Wrap 运行相位顺序执行顺序恒为 Input → Wrap → Output与注册顺序无关移除remove()使处理器不再触发同名处理器注册多次时只移除首次出现对从未注册的处理器是 no-op通过注册时返回的引用可移除适配过的 Input/Output 处理器错误传播terminal 的错误穿过中间件到达调用方中间件的错误到达调用方中间件可捕获并重抛不同错误错误转换InterruptError/InterruptException穿过透传中间件而不被吞掉生成器清理finally 保证terminal 抛出时中间件finally块运行多层中间件栈的finally块以逆序内层先运行消费者放弃迭代close/aclose时finally块运行close 时多层finally块全部运行。registry.test.ts 对上述每一条都给出了直接断言例如短路时 terminal 与内层处理器都不被调用、多层finally逆序执行a-finally → b-finally → c-finally、放弃生成器时所有finally运行等。9.2 InvokeModelStage 需求处理器在每次模型调用时被调用收到字段正确的 contextagent、messages、systemPrompt、toolSpecs、toolChoice、invocationState透传处理器不改变 agent 行为多个中间件按注册顺序组合可修改systemPrompt与toolSpecs且内层可见context 修改不改变原始对象可短路返回合成结果短路时模型不被调用短路结果作为模型调用结果使用BeforeModelCallEvent在中间件执行前触发AfterModelCallEvent在中间件完成后触发两者在短路时也触发Output 中间件可转换结果stopReason、message、usage、metrics转换结果是权威的也是追加到agent.messages的内容将stopReason从tool_use改为end_turn可阻止工具调度modelState不暴露于中间件 contextagent 在链前快照、链后写回 provider 变更中间件不能读写模型状态即使经由agent逃生舱写入会被覆盖agent 层面 Input → Wrap → Output 执行顺序恒成立瞬态错误重试中间件捕获错误后再次调用next。9.3 ExecuteToolStage 需求处理器在每次工具执行时被调用context 字段正确agent、tool、toolUse、invocationState透传不改变行为按注册顺序组合可修改工具入参工具收到修改后的参数context 修改不改变原对象可短路返回 Mock 结果短路时真实工具不被调用短路结果进入会话BeforeToolCallEvent/AfterToolCallEvent在中间件前后触发短路时也触发短路时AfterToolCallEvent.result包含中间件提供的结果Input 相位转换执行前的工具 contextOutput 相位转换执行后的工具结果工具的错误穿过中间件传播典型用例缓存插件。9.4 跨语言实现要点Python SDKPython 实现strands-py/src/strands/_middleware/README.md遵循同一行为规格并存在有意的差异结果编码TS 通过yield*传播 async generator 的 return 值Python async generator 无法 return因此最后一个 yield 的事件即结果ModelStopReason、ToolResultEvent、EventLoopStopEventOutput 相位包装Python 的 Output 处理器接收并返回MiddlewareResult包装registry 在调用前包装结果事件、返回后解包回流Wrap/Input 处理器仍处理原始事件与 contextPer-stage 结果类型Python 直接使用底层事件无独立包装类中断ExecuteToolContext.interrupt(...)与AgentStreamContext.interrupt(...)镜像 TS 的MiddlewareInterruptible契约中断 ID 为v1:middleware_execute_tool:toolUseId:uuid5(name)与v1:middleware_agent_stream:uuid5(name)在恢复间确定性一致Python 的Interrupt类型没有source字段消费者通过 ID 前缀区分来源防御性拷贝context 字段messages、system_prompt、tool_specs、tool_choice构建时深拷贝invocation_state按引用共享model_state完全排除Hook 驱动的重试会重跑中间件链AfterToolCallEvent设置retry True时整条链重建并重新调用有状态的中间件缓存、限流、遥测计数每次尝试运行一次——与中间件重试对 Hooks 不可见恰好相反无移除/清理Python 中间件注册后不能移除与 Python hook 系统一致_middleware/包不在公共 API 内内部消费者经由agent._middleware_registry.add_middleware(...)访问未知工具仍走链模型调用注册表中不存在的工具时链仍运行且ExecuteToolContext.tool为Noneterminal 产生 Unknown tool 错误结果工具异常在 terminal 内捕获tool.stream()的原始异常在 terminal 内转换为错误ToolResultEvent中间件观察到的总是结果而非抛出的异常InterruptException被重抛系统提示词联合类型InvokeModelContext.system_prompt是str | list[SystemContentBlock] | Noneterminal 通过split_system_prompt()分解为Model.stream()所需的两参形式。十、实战编写可复用的中间件10.1 缓存中间件短路模式import { ExecuteToolStage, InvokeModelStage } from strands // 或从 ./src/index.js 导入 // 工具缓存相同入参第二次直接返回 const toolCache new Mapstring, ExecuteToolResult[result]() agent.addMiddleware(ExecuteToolStage, async function* (context, next) { const key JSON.stringify(context.toolUse.input) const cached toolCache.get(key) if (cached) { return { result: cached } // 短路真实工具不被调用 } const { result } yield* next(context) toolCache.set(key, result) return { result } })10.2 限流中间件短路 Hooks 仍然触发agent.addMiddleware(InvokeModelStage, async function* (context, next) { if (rateLimitExceeded()) { return { result: syntheticThrottledResult() } // 模型不被调用 } return yield* next(context) })注意即使短路BeforeModelCallEvent/AfterModelCallEvent依然触发——监控 Hook 仍能看到这次尝试。10.3 重试中间件多次调用 nextagent.addMiddleware(InvokeModelStage, async function* (context, next) { for (let attempt 1; attempt 3; attempt) { try { return yield* next(context) } catch (error) { if (!isTransient(error) || attempt 3) throw error await sleep(backoff(attempt)) } } })对 Hooks 而言这只是一次调用按尝试次数的可观测性应注册在同一阶段的中间件中。10.4 人工审批门中间件中断agent.addMiddleware(ExecuteToolStage, async function* (context, next) { const { response } context.interrupt{ approved: boolean }({ name: confirm_destructive_action, reason: 该工具会删除生产数据请人工确认, }) if (!response.approved) { return { result: new ToolResultBlock({ toolUseId: context.toolUse.toolUseId, status: error, content: [new TextBlock(已取消)] }) } } return yield* next(context) })10.5 通过插件注册中间件ContextInjector 插件展示了标准模式插件在initAgent中向InvokeModelStage.Input注册注入中间件TS 见 plugin.tsPython 见 plugin.py。由于中间件由插件贡献同一相位内插件注册顺序仍然显著。十一、元数据传输未来设计当前不存在任何元数据字段但包装器被设计为能够承载它们。文档记录了这一意图以便在元数据加入时各实现保持一致。中间件处理器可以使用 context 与结果包装器上的元数据跨相位通信——例如 Input 处理器注解一个请求使 Output 处理器可以对其采取行动。context 对象承载 Input 相位元数据结果包装器InvokeModelResult、MiddlewareResult承载 Output 相位元数据。Python 的MiddlewareResult包装器目前仅持有value即为 Output 相位预留了与 Input 同等的未来可扩展表面。十二、总结strands 的中间件系统围绕三个公开/内部阶段与 Input → Wrap → Output 固定相位顺序构建先注册者最外层Hooks 始终在链外触发以保证短路与重试语义下的可观测性modelState被彻底隔离、invocationState按引用共享、per-call 模型替换与遥测记录后置状态等设计共同保证了中间件转换即事实。行为需求清单Registry Composition、InvokeModelStage、ExecuteToolStage、Middleware-Initiated Interrupts作为跨 TS/Python 双实现的契约每一节均可对照 registry.test.ts 与 agent-middleware.test.ts 等测试逐一验证。读者可以沿 types.ts、stages.ts、registry.ts 三个文件深入阅读完整实现并参考 Python 侧规格 README.md 了解双语言差异与内部演进方向。赞分享人工智能大模型AI AgentAgent 框架多智能体工具调用MCP 服务【免费下载链接】harness-sdkBuild an agent harness and control it end-to-end. Open-source SDK for production AI agents in Python TypeScript - any model, any cloud.项目地址https://gitcode.com/GitHub_Trending/sdkpython13/harness-sdk点击查看免费下载相关推荐GSD /gsd:ui-phase 工作流深度解析用 UI-SPEC 设计契约锁定前端阶段的视觉决策GSD /gsd:ui phase 工作流深度解析用 UI SPEC 设计契约锁定前端阶段的视觉决策 导读 /gsd:ui phase 是 get shit人工智能AI 应用提示工程开发工具工作流自动化AI Agentdbt-docs-server REST API 契约体系深度解析ADR 决策、端点契约与 Parquet 快照模型dbt docs server REST API 契约体系深度解析ADR 决策、端点契约与 Parquet 快照模型 导读 dbt docs server 是数据工程ETLCLItheHarvester 5.0.0 语义契约深度解析产品边界、执行模型与证据领域语言theHarvester 5.0.0 语义契约深度解析产品边界、执行模型与证据领域语言 本文围绕仓库根目录下的 CONTEXT.md https://link网络安全渗透测试上一篇HakuNeko终极指南快速掌握跨平台漫画动漫下载技巧下一篇如何轻松优化Windows内存这款轻量级工具是终极解决方案创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考