|
|
<!doctypehtml><html lang=zh-CN><meta charset=UTF-8><meta content=width=device-width name=viewport><meta media="(prefers-color-scheme: light)" content=#222 name=theme-color><meta media="(prefers-color-scheme: dark)" content=#222 name=theme-color><meta content="Hexo 7.3.0" name=generator><link href=https://cdnjs.cloudflare.com rel=preconnect><link href=/images/apple-touch-icon-next.png rel=apple-touch-icon sizes=180x180><link href=/images/favicon-32x32-next.png rel=icon sizes=32x32 type=image/png><link href=/images/favicon-16x16-next.png rel=icon sizes=16x16 type=image/png><link color=#222 href=/images/logo.svg rel=mask-icon><meta content=2X6S9P7CAjXjVvw8YyQR8pCu-B0oEu7O9quLgxXuWyA name=google-site-verification><meta content=dV8JGNzi0c name=baidu-site-verification><script>!function(){var t=!1;function e(){var e;t||(t=!0,(e=document.createElement("script")).setAttribute("data-ad-client","ca-pub-7480618470784058"),e.async=!0,e.src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js",document.head.appendChild(e))}function a(){"requestIdleCallback"in window?window.requestIdleCallback(e,{timeout:5e3}):window.setTimeout(e,5e3)}"complete"===document.readyState?a():window.addEventListener("load",a,{once:!0})}()</script><link href=/css/main.css rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.0/css/all.min.css integrity=sha256-VHqXKFhhMxcpubYf9xiWdCiojEbY9NexQ4jh8AxbvcM= rel=stylesheet><link crossorigin=anonymous href=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/6.1.14/fancybox/fancybox.css integrity=sha256-mdaMxrItEW6hMIVJJNPKuX0B7KCSJQVEv3mVY4N2we4= rel=stylesheet><script class=next-config data-name=main type=application/json>{"hostname":"nicksxs.me","root":"/","images":"/images","scheme":"Pisces","darkmode":true,"version":"8.29.0","exturl":false,"sidebar":{"position":"left","width_expanded":320,"width_dual_column":240,"display":"post","padding":18,"offset":12},"hljswrap":true,"codeblock":{"theme":{"light":"default","dark":"stackoverflow-dark"},"prism":{"light":"prism","dark":"prism-dark"},"copy_button":{"enable":true,"style":"default"},"fold":{"enable":false,"height":500},"language":false},"bookmark":{"enable":false,"color":"#222","save":"auto"},"mediumzoom":false,"pangu":false,"comments":{"style":"tabs","active":null,"storage":true,"lazyload":true,"nav":null},"stickytabs":false,"motion":{"enable":false,"async":false,"duration":200,"transition":{"menu_item":"fadeInDown","post_block":"fadeIn","post_header":"fadeInDown","post_body":"fadeInDown","coll_header":"fadeInLeft","sidebar":"fadeInUp"}},"prism":false,"i18n":{"placeholder":"搜索...","empty":"没有找到任何搜索结果:${query}","hits_time":"找到 ${hits} 个搜索结果(用时 ${time} 毫秒)","hits":"找到 ${hits} 个搜索结果"},"algolia":{"appID":"663Q99OQQD","apiKey":"e461a1371d12cec1491c1153b288a9ed","indexName":"nicksxs","hits":{"per_page":10}}}</script><script defer src=/js/config.js></script><meta content="从源码搭建 Pi Agent 调试环境,并沿着 cli.ts、sdk.ts、agent.ts 和 agent-loop.ts 的真实调用链,分析模型配置、StreamFn 注入、双层 Agent Loop、流式响应和工具调用。" name=description><meta content=article property=og:type><meta content="Pi Agent 源码解析(一):从调试环境到双层主循环" property=og:title><meta content=https://nicksxs.me/2026/09/13/Pi-Agent-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E4%BB%8E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E5%88%B0%E5%8F%8C%E5%B1%82%E4%B8%BB%E5%BE%AA%E7%8E%AF/index.html property=og:url><meta content="Nicksxs's Blog" property=og:site_name><meta content="从源码搭建 Pi Agent 调试环境,并沿着 cli.ts、sdk.ts、agent.ts 和 agent-loop.ts 的真实调用链,分析模型配置、StreamFn 注入、双层 Agent Loop、流式响应和工具调用。" property=og:description><meta content=zh_CN property=og:locale><meta content=2026-09-13T12:00:00.000Z property=article:published_time><meta content=2026-09-13T12:00:00.000Z property=article:modified_time><meta content=Nicksxs property=article:author><meta content=LLM property=article:tag><meta content=Agent property=article:tag><meta content=TypeScript property=article:tag><meta content=summary name=twitter:card><link href=https://nicksxs.me/2026/09/13/Pi-Agent-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E4%BB%8E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E5%88%B0%E5%8F%8C%E5%B1%82%E4%B8%BB%E5%BE%AA%E7%8E%AF/ rel=canonical><script class=next-config data-name=page type=application/json>{"sidebar":"","isHome":false,"isPost":true,"lang":"zh-CN","comments":true,"permalink":"https://nicksxs.me/2026/09/13/Pi-Agent-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E4%BB%8E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E5%88%B0%E5%8F%8C%E5%B1%82%E4%B8%BB%E5%BE%AA%E7%8E%AF/","path":"2026/09/13/Pi-Agent-源码解析-从调试环境到双层主循环/","title":"Pi Agent 源码解析(一):从调试环境到双层主循环"}</script><script class=next-config data-name=calendar type=application/json>""</script><title>Pi Agent 源码解析(一):从调试环境到双层主循环 | Nicksxs's Blog</title><script async src=https://www.googletagmanager.com/gtag/js?id=UA-61358619-1></script><script class=next-config data-name=google_analytics type=application/json>{"tracking_id":"UA-61358619-1","only_pageview":false,"measure_protocol_api_secret":null}</script><script defer src=/js/third-party/analytics/google-analytics.js></script><script defer src=/js/third-party/analytics/baidu-analytics.js></script><script async src=https://hm.baidu.com/hm.js?20f33b3c0c0eff9b1522999c0015646d></script><script crossorigin=anonymous defer integrity=sha256-QnJV4HT5r8i89ovZGRD86iynZqHerEbT2xt69vDM36I= src=https://cdnjs.cloudflare.com/ajax/libs/fancyapps-ui/6.1.14/fancybox/fancybox.umd.js></script><script defer src=/js/utils.js></script><script defer src=/js/sidebar.js></script><script defer src=/js/next-boot.js></script><script data-algolia-integrity=sha256-o49zoHLDPBm9WLg6AiliAVi3axCRTqe3ltTxLvhzAVg= data-algolia-src=https://cdnjs.cloudflare.com/ajax/libs/algoliasearch/5.36.0/lite/builds/browser.umd.js data-search-src=/js/third-party/search/algolia-search.js src=/js/third-party/search/algolia-search-loader.js></script><script defer src=/js/third-party/fancybox.js></script><noscript><link href=/css/noscript.css rel=stylesheet></noscript><link title="Nicksxs's Blog" href=/atom.xml rel=alternate type=application/atom+xml><body itemscope itemtype=http://schema.org/WebPage><div class=headband></div><main class=main><div class=column><header class=header itemscope itemtype=http://schema.org/WPHeader><div class=site-brand-container><div class=site-nav-toggle><div aria-label=切换导航栏 class=toggle role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div></div><div class=site-meta><a class=brand href=/ rel=start><i class=logo-line></i><p class=site-title>Nicksxs's Blog</p><i class=logo-line></i></a><p class=site-subtitle itemprop=description>Java 运行时、生产诊断与 AI 工程实践</div><div class=site-nav-right><div class="toggle popup-trigger" aria-label=搜索 role=button><i class="fa fa-search fa-fw fa-lg"></i></div></div></div><nav class=site-nav><ul class="main-menu menu"><li class="menu-item menu-item-home"><a href=/ rel=section><i class="fa fa-home fa-fw"></i>首页</a><li class="menu-item menu-item-专题"><a href=/topics/ rel=section><i class="fa fa-layer-group fa-fw"></i>专题</a><li class="menu-item menu-item-工具"><a href=/tools/ rel=section><i class="fa fa-toolbox fa-fw"></i>工具</a><li class="menu-item menu-item-about"><a href=/about/ rel=section><i class="fa fa-user fa-fw"></i>关于我</a><li class="menu-item menu-item-tags"><a href=/tags/ rel=section><i class="fa fa-tags fa-fw"></i>标签</a><li class="menu-item menu-item-categories"><a href=/categories/ rel=section><i class="fa fa-th fa-fw"></i>分类</a><li class="menu-item menu-item-archives"><a href=/archives/ rel=section><i class="fa fa-archive fa-fw"></i>归档</a><li class="menu-item menu-item-search"><a class=popup-trigger role=button><i class="fa fa-search fa-fw"></i>搜索</a></ul></nav><div class=search-pop-overlay><div class="popup search-popup"><div class=search-header><span class=search-icon><i class="fa fa-search"></i></span><div class=search-input-container><input autocapitalize=off autocomplete=off class=search-input maxlength=80 placeholder=搜索... spellcheck=false type=search></div><span class=popup-btn-close role=button><i class="fa fa-times-circle"></i></span></div><div class=search-result-container><div class=search-result-icon><i class="fab fa-algolia fa-5x"></i></div></div></div></div></header><aside class=sidebar><div class="sidebar-inner sidebar-nav-active sidebar-toc-active"><ul class=sidebar-nav><li class=sidebar-nav-toc>文章目录<li class=sidebar-nav-overview>站点概览</ul><div class=sidebar-panel-container><div class="post-toc-wrap sidebar-panel"><div class="post-toc animated"><ol class=nav><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%B8%80%E3%80%81%E5%85%88%E7%9C%8B%E9%A1%B9%E7%9B%AE%E5%88%86%E5%B1%82><span class=nav-number>1.</span> <span class=nav-text>一、先看项目分层</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%BA%8C%E3%80%81%E6%90%AD%E5%BB%BA%E6%BA%90%E7%A0%81%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83><span class=nav-number>2.</span> <span class=nav-text>二、搭建源码调试环境</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#1-Node-js-%E7%89%88%E6%9C%AC><span class=nav-number>2.1.</span> <span class=nav-text>1. Node.js 版本</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#2-Clone-%E4%B8%8E%E5%AE%89%E8%A3%85%E4%BE%9D%E8%B5%96><span class=nav-number>2.2.</span> <span class=nav-text>2. Clone 与安装依赖</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#3-%E4%B8%8B%E8%BD%BD%E6%A8%A1%E5%9E%8B%E7%9B%AE%E5%BD%95><span class=nav-number>2.3.</span> <span class=nav-text>3. 下载模型目录</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#4-%E9%AA%8C%E8%AF%81%E6%A8%A1%E5%9E%8B%E6%95%B0%E6%8D%AE><span class=nav-number>2.4.</span> <span class=nav-text>4. 验证模型数据</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%B8%89%E3%80%81%E9%85%8D%E7%BD%AE%E6%A8%A1%E5%9E%8B%E5%92%8C%E5%AF%86%E9%92%A5><span class=nav-number>3.</span> <span class=nav-text>三、配置模型和密钥</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E4%B8%80%E4%B8%AA-OpenAI-%E5%85%BC%E5%AE%B9%E6%9C%8D%E5%8A%A1%E7%9A%84%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90><span class=nav-number>3.1.</span> <span class=nav-text>一个 OpenAI 兼容服务的配置例子</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%9B%9B%E3%80%81%E9%85%8D%E7%BD%AE-VS-Code-%E8%B0%83%E8%AF%95%E5%85%A5%E5%8F%A3><span class=nav-number>4.</span> <span class=nav-text>四、配置 VS Code 调试入口</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%BA%94%E3%80%81%E4%B8%80%E6%AC%A1%E8%AF%B7%E6%B1%82%E7%9A%84%E5%AE%8C%E6%95%B4%E8%B0%83%E7%94%A8%E9%93%BE><span class=nav-number>5.</span> <span class=nav-text>五、一次请求的完整调用链</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%85%AD%E3%80%81Agent-%E5%88%9D%E5%A7%8B%E5%8C%96%EF%BC%9AstreamFn-%E5%9C%A8-sdk-ts-%E4%B8%AD%E6%B3%A8%E5%85%A5><span class=nav-number>6.</span> <span class=nav-text>六、Agent 初始化:streamFn 在 sdk.ts 中注入</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#1-%E5%85%88%E5%AE%89%E8%A3%85%E4%B8%80%E4%B8%AA%E5%85%BC%E5%AE%B9%E6%80%A7%E9%BB%98%E8%AE%A4%E5%80%BC><span class=nav-number>6.1.</span> <span class=nav-text>1. 先安装一个兼容性默认值</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#2-%E5%AE%9E%E9%99%85-Agent-%E4%BD%BF%E7%94%A8%E7%9A%84%E6%98%AF%E6%98%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E7%9A%84-streamFn><span class=nav-number>6.2.</span> <span class=nav-text>2. 实际 Agent 使用的是显式注入的 streamFn</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#3-StreamFn-%E7%9A%84%E5%A5%91%E7%BA%A6><span class=nav-number>6.3.</span> <span class=nav-text>3. StreamFn 的契约</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#4-ModelRuntime-%E5%86%8D%E9%80%89%E6%8B%A9%E5%85%B7%E4%BD%93-Provider><span class=nav-number>6.4.</span> <span class=nav-text>4. ModelRuntime 再选择具体 Provider</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%B8%83%E3%80%81%E4%BB%8E-AgentSession-%E8%BF%9B%E5%85%A5-Agent-Core><span class=nav-number>7.</span> <span class=nav-text>七、从 AgentSession 进入 Agent Core</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%85%AB%E3%80%81runAgentLoop%EF%BC%9A%E5%87%86%E5%A4%87%E4%B8%8A%E4%B8%8B%E6%96%87%E5%92%8C%E4%BA%8B%E4%BB%B6><span class=nav-number>8.</span> <span class=nav-text>八、runAgentLoop:准备上下文和事件</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E4%B9%9D%E3%80%81%E6%A0%B8%E5%BF%83%EF%BC%9A%E4%B8%A4%E5%B1%82-Agent-Loop-%E5%88%B0%E5%BA%95%E5%9C%A8%E5%BE%AA%E7%8E%AF%E4%BB%80%E4%B9%88><span class=nav-number>9.</span> <span class=nav-text>九、核心:两层 Agent Loop 到底在循环什么</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#1-%E5%86%85%E5%B1%82%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%AE%8C%E6%88%90%E5%BD%93%E5%89%8D%E4%BB%BB%E5%8A%A1%E9%93%BE><span class=nav-number>9.1.</span> <span class=nav-text>1. 内层循环:完成当前任务链</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#2-%E5%A4%96%E5%B1%82%E5%BE%AA%E7%8E%AF%EF%BC%9A%E5%A4%84%E7%90%86-Agent-%E5%81%9C%E6%AD%A2%E5%90%8E%E7%9A%84-follow-up><span class=nav-number>9.2.</span> <span class=nav-text>2. 外层循环:处理 Agent 停止后的 follow-up</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#3-prepareNextTurn-%E4%B8%BA%E4%BB%80%E4%B9%88%E6%94%BE%E5%9C%A8%E8%AF%B7%E6%B1%82%E5%89%8D><span class=nav-number>9.3.</span> <span class=nav-text>3. prepareNextTurn 为什么放在请求前</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#4-length-%E6%97%B6%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E6%89%A7%E8%A1%8C%E5%B7%A5%E5%85%B7><span class=nav-number>9.4.</span> <span class=nav-text>4. length 时为什么不执行工具</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%8D%81%E3%80%81%E4%B8%80%E6%AC%A1-LLM-%E6%B5%81%E5%BC%8F%E5%93%8D%E5%BA%94%E6%98%AF%E6%80%8E%E6%A0%B7%E8%A2%AB%E6%B6%88%E8%B4%B9%E7%9A%84><span class=nav-number>10.</span> <span class=nav-text>十、一次 LLM 流式响应是怎样被消费的</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%8D%81%E4%B8%80%E3%80%81%E5%B7%A5%E5%85%B7%E8%B0%83%E7%94%A8%EF%BC%9A%E9%80%89%E6%8B%A9%E3%80%81%E6%A0%A1%E9%AA%8C%E3%80%81%E6%89%A7%E8%A1%8C%E4%B8%8E%E5%9B%9E%E5%A1%AB><span class=nav-number>11.</span> <span class=nav-text>十一、工具调用:选择、校验、执行与回填</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%8D%81%E4%BA%8C%E3%80%81%E4%BA%8B%E4%BB%B6%E5%A6%82%E4%BD%95%E6%9B%B4%E6%96%B0-Agent-%E7%8A%B6%E6%80%81><span class=nav-number>12.</span> <span class=nav-text>十二、事件如何更新 Agent 状态</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%8D%81%E4%B8%89%E3%80%81%E9%80%82%E5%90%88%E5%AE%9E%E9%99%85%E8%B0%83%E8%AF%95%E7%9A%84%E4%B8%80%E7%BB%84%E6%96%AD%E7%82%B9><span class=nav-number>13.</span> <span class=nav-text>十三、适合实际调试的一组断点</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E6%A8%A1%E5%9E%8B%E6%B2%A1%E6%9C%89%E5%87%BA%E7%8E%B0%E6%88%96%E8%AE%A4%E8%AF%81%E5%A4%B1%E8%B4%A5><span class=nav-number>13.1.</span> <span class=nav-text>模型没有出现或认证失败</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E8%AF%B7%E6%B1%82%E5%8F%82%E6%95%B0%E3%80%81%E4%BB%A3%E7%90%86%E5%9C%B0%E5%9D%80%E6%88%96-header-%E4%B8%8D%E7%AC%A6%E5%90%88%E9%A2%84%E6%9C%9F><span class=nav-number>13.2.</span> <span class=nav-text>请求参数、代理地址或 header 不符合预期</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#Agent-%E4%B8%BA%E4%BB%80%E4%B9%88%E7%BB%A7%E7%BB%AD%E6%88%96%E5%81%9C%E6%AD%A2><span class=nav-number>13.3.</span> <span class=nav-text>Agent 为什么继续或停止</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E5%B7%A5%E5%85%B7%E4%B8%BA%E4%BB%80%E4%B9%88%E6%B2%A1%E6%9C%89%E6%89%A7%E8%A1%8C><span class=nav-number>13.4.</span> <span class=nav-text>工具为什么没有执行</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%8D%81%E5%9B%9B%E3%80%81%E6%80%BB%E7%BB%93><span class=nav-number>14.</span> <span class=nav-text>十四、总结</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%8F%82%E8%80%83%E6%BA%90%E7%A0%81><span class=nav-number>15.</span> <span class=nav-text>参考源码</span></a></ol></div></div><div class="site-overview-wrap sidebar-panel"><div class="site-author animated" itemprop=author itemscope itemtype=http://schema.org/Person><img alt=Nicksxs class=site-author-image itemprop=image src=/uploads/avatar.jpg><p class=site-author-name itemprop=name>Nicksxs<div class=site-description itemprop=description>Nicksxs 的技术博客,深入 Java 运行时与生产诊断、低成本大模型推理和 Agent 工程实践。</div></div><div class="site-state-wrap animated"><nav class=site-state><div class="site-state-item site-state-posts"><a href=/archives/><span class=site-state-item-count>394</span> <span class=site-state-item-name>日志</span></a></div><div class="site-state-item site-state-categories"><a href=/categories/><span class=site-state-item-count>181</span> <span class=site-state-item-name>分类</span></a></div><div class="site-state-item site-state-tags"><a href=/tags/><span class=site-state-item-count>347</span> <span class=site-state-item-name>标签</span></a></div></nav></div><div class="links-of-author animated"><span class=links-of-author-item><a rel="noopener me" title="GitHub → https://github.com/nicksxs" href=https://github.com/nicksxs target=_blank><i class="fab fa-github fa-fw"></i>GitHub</a> </span><span class=links-of-author-item><a rel="noopener me" title="E-Mail → mailto:nicksxs1202@gmail.com" href=mailto:nicksxs1202@gmail.com target=_blank><i class="fa fa-envelope fa-fw"></i>E-Mail</a></span></div><div class="cc-license animated" itemprop=license><a class=cc-opacity href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><img alt="Creative Commons" src=https://cdnjs.cloudflare.com/ajax/libs/creativecommons-vocabulary/2020.11.3/assets/license_badges/small/by_nc_sa.svg></a></div><div class="widget-wrap sidebar-tag-cloud"><div class=tag-cloud-tags><a class=tag-cloud-10 href=/tags/Java/ style=font-size:16px>Java</a><a class=tag-cloud-9 href=/tags/leetcode/ style=font-size:15.62px>leetcode</a><a class=tag-cloud-8 href=/tags/LLM/ style=font-size:15.23px>LLM</a><a class=tag-cloud-8 href=/tags/%E7%94%9F%E6%B4%BB/ style=font-size:14.85px>生活</a><a class=tag-cloud-7 href=/tags/%E9%A2%98%E8%A7%A3/ style=font-size:14.46px>题解</a><a class=tag-cloud-6 href=/tags/SpringBoot/ style=font-size:14.08px>SpringBoot</a><a class=tag-cloud-6 href=/tags/MySQL/ style=font-size:14.08px>MySQL</a><a class=tag-cloud-5 href=/tags/C/ style=font-size:13.69px>C++</a><a class=tag-cloud-5 href=/tags/Redis/ style=font-size:13.31px>Redis</a><a class=tag-cloud-4 href=/tags/Tomcat/ style=font-size:12.92px>Tomcat</a><a class=tag-cloud-4 href=/tags/Mybatis/ style=font-size:12.92px>Mybatis</a><a class=tag-cloud-3 href=/tags/%E5%B0%8F%E6%8A%80%E5%B7%A7/ style=font-size:12.54px>小技巧</a><a class=tag-cloud-2 href=/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ style=font-size:12.15px>数据结构</a><a class=tag-cloud-2 href=/tags/%E6%BA%90%E7%A0%81/ style=font-size:12.15px>源码</a><a class=tag-cloud-2 href=/tags/MQ/ style=font-size:11.77px>MQ</a><a class=tag-cloud-2 href=/tags/%E6%B6%88%E6%81%AF%E9%98%9F%E5%88%97/ style=font-size:11.77px>消息队列</a><a class=tag-cloud-2 href=/tags/RocketMQ/ style=font-size:11.77px>RocketMQ</a><a class=tag-cloud-2 href=/tags/%E8%BF%90%E5%8A%A8/ style=font-size:11.77px>运动</a><a class=tag-cloud-1 href=/tags/php/ style=font-size:11.38px>php</a><a class=tag-cloud-0 href=/tags/Spring/ style=font-size:11px>Spring</a><a class=tag-cloud-0 href=/tags/linux/ style=font-size:11px>linux</a><a class=tag-cloud-0 href=/tags/Docker/ style=font-size:11px>Docker</a><a class=tag-cloud-0 href=/tags/%E5%87%8F%E8%82%A5/ style=font-size:11px>减肥</a><a class=tag-cloud-0 href=/tags/%E8%B7%91%E6%AD%A5/ style=font-size:11px>跑步</a></div></div><div class="affiliate-creative affiliate-creative--sidebar" data-affiliate-id=tencent-workbuddy><a aria-label="腾讯 WorkBuddy" rel="sponsored noopener" href=https://cloud.tencent.com/act/cps/redirect?redirect=6871&cps_key=2e33a1eedb380997d03618b4a5ba6ca7&from=console target=_blank><img alt="腾讯 WorkBuddy AI 办公工作台" decoding=async height=389 loading=lazy src=/uploads/affiliate/tencent-workbuddy.webp width=690> </a><span class=affiliate-disclosure>推广</span></div></div></div></div><div class="sidebar-inner sidebar-blogroll"><div class="links-of-blogroll animated"><div class=links-of-blogroll-title><i class="fa fa-globe fa-fw"></i> 链接</div><ul class=links-of-blogroll-list><li class=links-of-blogroll-item><a href=https://covermusic.cn/ rel=noopener target=_blank title=https://covermusic.cn>69伙伴</a></ul></div></div></aside></div><div class="main-inner post posts-expand"><div class=post-block><article class=post-content itemscope itemtype=http://schema.org/Article lang=zh-CN><link href=https://nicksxs.me/2026/09/13/Pi-Agent-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E4%BB%8E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E5%88%B0%E5%8F%8C%E5%B1%82%E4%B8%BB%E5%BE%AA%E7%8E%AF/ itemprop=mainEntityOfPage><span hidden itemprop=author itemscope itemtype=http://schema.org/Person><meta content=/uploads/avatar.jpg itemprop=image><meta content=Nicksxs itemprop=name></span><span hidden itemprop=publisher itemscope itemtype=http://schema.org/Organization><meta content="Nicksxs's Blog" itemprop=name><meta content="Nicksxs 的技术博客,深入 Java 运行时与生产诊断、低成本大模型推理和 Agent 工程实践。" itemprop=description></span><span hidden itemprop=post itemscope itemtype=http://schema.org/CreativeWork><meta content="Pi Agent 源码解析(一):从调试环境到双层主循环 | Nicksxs's Blog" itemprop=name><meta content="从源码搭建 Pi Agent 调试环境,并沿着 cli.ts、sdk.ts、agent.ts 和 agent-loop.ts 的真实调用链,分析模型配置、StreamFn 注入、双层 Agent Loop、流式响应和工具调用。" itemprop=description></span><header class=post-header><h1 itemprop="name headline" class=post-title>Pi Agent 源码解析(一):从调试环境到双层主循环</h1><div class=post-meta-container><div class=post-meta><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-calendar"></i> </span><span class=post-meta-item-text>发表于</span> <time itemprop="dateCreated datePublished" title="创建时间:2026-09-13 20:00:00" datetime=2026-09-13T20:00:00+08:00>2026-09-13</time> </span><span class=post-meta-item><span class=post-meta-item-icon><i class="far fa-folder"></i> </span><span class=post-meta-item-text>分类于</span> <span itemprop=about itemscope itemtype=http://schema.org/Thing><a href=/categories/LLM/ itemprop=url rel=index><span itemprop=name>LLM</span></a> </span></span><span class="post-meta-item cloudflare-page-counter" data-title="Pi Agent 源码解析(一):从调试环境到双层主循环" data-increment=true data-path=/2026/09/13/Pi-Agent-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E4%BB%8E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E5%88%B0%E5%8F%8C%E5%B1%82%E4%B8%BB%E5%BE%AA%E7%8E%AF/ title=阅读次数><span class=post-meta-item-icon><i class="far fa-eye"></i> </span><span class=post-meta-item-text>阅读次数:</span> <span class=cloudflare-page-counter-count></span></span></div><div class=post-description>从源码搭建 Pi Agent 调试环境,并沿着 cli.ts、sdk.ts、agent.ts 和 agent-loop.ts 的真实调用链,分析模型配置、StreamFn 注入、双层 Agent Loop、流式响应和工具调用。</div></div></header><div class=post-body itemprop=articleBody><p>最近一段时间我一直在使用 Pi。它的交互界面看起来很简单,但代码里把模型适配、会话管理、上下文处理、工具执行和终端 UI 分成了比较清楚的几层,很适合拿来理解一个 Coding Agent 到底是怎么运转的。<p>前面已经写过一篇 <a href=/2026/08/09/%E6%9D%A5%E5%AD%A6%E4%B9%A0%E4%B8%8Bpi-agent%E7%9A%84%E5%8E%9F%E7%90%86/>Pi Agent 工具系统:Read、Write、Edit 与 Bash 的实现原理</a>,那篇关注 Agent 的“手脚”,本文继续向下看驱动这些工具反复工作的主循环。<p>这篇先从源码调试环境开始,然后沿着一次真实请求的调用链,重点看下面几个问题:<ol><li>Pi 的源码怎样安装、补齐模型数据并启动调试;<li>模型的 <code>baseUrl</code> 和 <code>apiKey</code> 从哪里读取;<li><code>Agent</code> 在哪里初始化,<code>streamFn</code> 又是在哪里注入的;<li><code>agent-loop.ts</code> 中的两层循环分别解决什么问题;<li>LLM 的流式事件如何变成 Agent 事件;<li>模型发起工具调用以后,工具结果如何回到上下文并触发下一轮请求。</ol><p>本文对应我本地阅读的源码版本:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line>repository: https://github.com/earendil-works/pi</span><br><span class=line>commit: 71dca871bc80b6bc97be37f0ca3189399d651fff</span><br><span class=line>describe: v0.85.1-67-g71dca871b</span><br><span class=line>date: 2026-09-11</span><br></pre></table></figure><p>Pi 的代码还在快速变化,后续如果发现文件名或行号对不上,应当先确认源码版本。本文代码片段均摘自上述提交;为了方便阅读,我只在少数位置加入了以 <code>// 解读:</code> 开头的注释,没有把真实逻辑改写成伪代码。<h2 id=一、先看项目分层><a class=headerlink href=#一、先看项目分层 title=一、先看项目分层></a>一、先看项目分层</h2><p>仓库根目录的开发文档给出的主要结构如下:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>packages/</span><br><span class=line> ai/ # LLM provider 抽象、模型目录以及各家 API 适配</span><br><span class=line> agent/ # Agent 状态、事件、主循环和工具执行</span><br><span class=line> tui/ # 终端 UI</span><br><span class=line> coding-agent/ # CLI、会话、配置、扩展、内置工具和交互模式</span><br></pre></table></figure><p>阅读时最容易混淆的是 <code>agent</code> 与 <code>coding-agent</code>:<ul><li><code>packages/agent</code> 是较底层、与具体模型供应商无关的 Agent Core;<li><code>packages/coding-agent</code> 是完整产品层,负责加载用户配置、模型运行时、会话、工具、扩展和 TUI;<li><code>packages/ai</code> 才真正知道 OpenAI、Anthropic、Google 等供应商的请求协议。</ul><p>所以 <code>agent-loop.ts</code> 不会直接调用 OpenAI SDK。它只认识一个抽象的 <code>StreamFn</code>,具体实现由上层注入。这是理解整套代码最关键的一点。<h2 id=二、搭建源码调试环境><a class=headerlink href=#二、搭建源码调试环境 title=二、搭建源码调试环境></a>二、搭建源码调试环境</h2><h3 id=1-Node-js-版本><a title="1. Node.js 版本" class=headerlink href=#1-Node-js-版本></a>1. Node.js 版本</h3><p>根目录 <code>package.json</code> 的要求不是笼统的 Node 22,而是:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line><span class=attr>"engines"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"node"</span><span class=punctuation>:</span> <span class=string>">=22.19.0"</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>因此建议先确认版本:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>node --version</span><br><span class=line>npm --version</span><br></pre></table></figure><p>我使用的是 Node 22。如果是较早的 22.x,也要确认小版本不低于 <code>22.19.0</code>。<h3 id=2-Clone-与安装依赖><a title="2. Clone 与安装依赖" class=headerlink href=#2-Clone-与安装依赖></a>2. Clone 与安装依赖</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br></pre><td class=code><pre><span class=line>git <span class=built_in>clone</span> https://github.com/earendil-works/pi.git</span><br><span class=line><span class=built_in>cd</span> pi</span><br><span class=line>npm ci</span><br></pre></table></figure><p>这里补充一下 <code>npm install</code> 和 <code>npm ci</code> 的区别。这两个通常是二选一,并不需要固定地先后执行:<ul><li><code>npm ci</code> 严格按照已有的 <code>package-lock.json</code> 做干净安装,适合复现仓库环境;<li><code>npm install</code> 更适合要调整依赖、更新 lockfile 的开发场景。</ul><p>如果只是阅读和调试源码,我更推荐 <code>npm ci</code>。仓库自己的开发规则还要求使用 <code>--ignore-scripts</code>,因此更谨慎的做法是:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>npm ci --ignore-scripts</span><br></pre></table></figure><p>如果确实需要仓库生命周期脚本,再在理解脚本用途后单独执行。<h3 id=3-下载模型目录><a title="3. 下载模型目录" class=headerlink href=#3-下载模型目录></a>3. 下载模型目录</h3><p>安装依赖以后还要补齐模型数据:<figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>npm run hydrate:model-data</span><br></pre></table></figure><p>根目录只是把命令转发给 <code>packages/ai</code>:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line><span class=attr>"hydrate:model-data"</span><span class=punctuation>:</span> <span class=string>"npm --prefix packages/ai run hydrate-model-data"</span><span class=punctuation>,</span></span><br><span class=line><span class=attr>"check:model-data"</span><span class=punctuation>:</span> <span class=string>"npm --prefix packages/ai run check:model-data"</span></span><br></pre></table></figure><p>而 <code>packages/ai/package.json</code> 里的实际命令是:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line><span class=attr>"hydrate-model-data"</span><span class=punctuation>:</span> <span class=string>"node scripts/generate-models.ts --strict --data-only"</span><span class=punctuation>,</span></span><br><span class=line><span class=attr>"check:model-data"</span><span class=punctuation>:</span> <span class=string>"node scripts/check-model-data.ts"</span></span><br></pre></table></figure><p><code>hydrate:model-data</code> 不是下载某个模型权重,它下载的是 Pi 内置模型目录所需的元数据,例如模型 ID、API 类型、上下文窗口、最大输出长度、价格和是否支持 reasoning 等。<p><code>generate-models.ts</code> 会访问多个在线目录,代码里可以直接看到这些请求:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br></pre><td class=code><pre><span class=line><span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">fetchNvidiaNimModelIds</span>(<span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">Map</span><<span class=built_in>string</span>, <span class=built_in>string</span>>> {</span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class=string>"Fetching models from NVIDIA NIM API..."</span>);</span><br><span class=line> <span class=keyword>const</span> response = <span class=keyword>await</span> <span class="title function_">fetch</span>(<span class=string>`<span class=subst>${NVIDIA_BASE_URL}</span>/models`</span>);</span><br><span class=line> <span class=keyword>if</span> (!response.<span class=property>ok</span>) <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">Error</span>(<span class=string>`NVIDIA NIM API returned <span class=subst>${response.status}</span>`</span>);</span><br><span class=line> <span class=comment>// ...</span></span><br><span class=line> } <span class=keyword>catch</span> (error) {</span><br><span class=line> <span class="variable language_">console</span>.<span class="title function_">error</span>(<span class=string>"Failed to fetch NVIDIA NIM models:"</span>, error);</span><br><span class=line> <span class=keyword>if</span> (generatorOptions.<span class=property>strict</span>) <span class=keyword>throw</span> error;</span><br><span class=line> <span class=keyword>return</span> <span class=keyword>new</span> <span class="title class_">Map</span>();</span><br><span class=line> }</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">fetchOpenRouterModels</span>(<span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">Model</span><<span class=built_in>any</span>>[]> {</span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class=string>"Fetching models from OpenRouter API..."</span>);</span><br><span class=line> <span class=keyword>const</span> response = <span class=keyword>await</span> <span class="title function_">fetch</span>(<span class=string>"https://openrouter.ai/api/v1/models"</span>);</span><br><span class=line> <span class=keyword>if</span> (!response.<span class=property>ok</span>) <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">Error</span>(<span class=string>`OpenRouter API returned <span class=subst>${response.status}</span>`</span>);</span><br><span class=line> <span class=comment>// ...</span></span><br><span class=line> }</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">loadModelsDevData</span>(<span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">Model</span><<span class=built_in>any</span>>[]> {</span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class=string>"Fetching models from models.dev API..."</span>);</span><br><span class=line> <span class=keyword>const</span> response = <span class=keyword>await</span> <span class="title function_">fetch</span>(<span class=string>"https://models.dev/api.json"</span>);</span><br><span class=line> <span class=keyword>if</span> (!response.<span class=property>ok</span>) <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">Error</span>(<span class=string>`models.dev API returned <span class=subst>${response.status}</span>`</span>);</span><br><span class=line> <span class=comment>// ...</span></span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><p>命令带有 <code>--strict</code>,某个关键数据源访问失败时会直接报错,而不是悄悄生成残缺目录。因此在部分网络环境下可能需要配置代理,也就是大家常说的“科学上网”。<p>数据不会直接覆盖旧目录。生成器先在临时目录写入并校验,成功后才替换 <code>src/providers/data</code>:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br></pre><td class=code><pre><span class=line><span class=keyword>const</span> providersDir = <span class="title function_">join</span>(packageRoot, <span class=string>"src/providers"</span>);</span><br><span class=line><span class=keyword>const</span> dataDir = <span class="title function_">join</span>(providersDir, <span class=string>"data"</span>);</span><br><span class=line><span class=keyword>const</span> stagingRoot = <span class="title function_">mkdtempSync</span>(<span class="title function_">join</span>(providersDir, <span class=string>".model-generation-"</span>));</span><br><span class=line><span class=keyword>const</span> stagedDataDir = <span class="title function_">join</span>(stagingRoot, <span class=string>"data"</span>);</span><br><span class=line><span class=keyword>const</span> previousDataDir = <span class="title function_">join</span>(stagingRoot, <span class=string>"previous-data"</span>);</span><br><span class=line></span><br><span class=line><span class=comment>// 先向 stagedDataDir 写入各 provider 的 JSON 和 manifest</span></span><br><span class=line><span class="title function_">validateModelDataDirectory</span>(modelDataStructure, stagedDataDir);</span><br><span class=line></span><br><span class=line><span class=keyword>const</span> hadPreviousData = <span class="title function_">existsSync</span>(dataDir);</span><br><span class=line><span class=keyword>if</span> (hadPreviousData) <span class="title function_">renameSync</span>(dataDir, previousDataDir);</span><br><span class=line><span class=keyword>try</span> {</span><br><span class=line> <span class="title function_">renameSync</span>(stagedDataDir, dataDir);</span><br><span class=line> <span class="title function_">validateGeneratedModelData</span>(packageRoot);</span><br><span class=line>} <span class=keyword>catch</span> (error) {</span><br><span class=line> <span class="title function_">rmSync</span>(dataDir, { <span class=attr>recursive</span>: <span class=literal>true</span>, <span class=attr>force</span>: <span class=literal>true</span> });</span><br><span class=line> <span class=keyword>if</span> (hadPreviousData && <span class="title function_">existsSync</span>(previousDataDir)) {</span><br><span class=line> <span class="title function_">renameSync</span>(previousDataDir, dataDir);</span><br><span class=line> }</span><br><span class=line> <span class=keyword>throw</span> error;</span><br><span class=line>}</span><br></pre></table></figure><p>这实际上是一个带回滚的目录级原子替换:新数据先通过校验,替换后再做一次整体校验;失败时恢复旧目录,避免半生成状态污染工作区。<h3 id=4-验证模型数据><a title="4. 验证模型数据" class=headerlink href=#4-验证模型数据></a>4. 验证模型数据</h3><figure class="highlight bash"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>npm run check:model-data</span><br></pre></table></figure><p>对应的检查入口很短:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line><span class=keyword>try</span> {</span><br><span class=line> <span class="title function_">validateGeneratedModelData</span>(packageRoot);</span><br><span class=line> <span class="variable language_">console</span>.<span class="title function_">log</span>(<span class=string>"Generated model data is valid."</span>);</span><br><span class=line>} <span class=keyword>catch</span> (error) {</span><br><span class=line> <span class="variable language_">console</span>.<span class="title function_">error</span>(error <span class=keyword>instanceof</span> <span class="title class_">Error</span> ? error.<span class=property>message</span> : <span class="title class_">String</span>(error));</span><br><span class=line> <span class="variable language_">console</span>.<span class="title function_">error</span>(</span><br><span class=line> <span class=string>"\nModel data is missing or stale. "</span> +</span><br><span class=line> <span class=string>"Run `npm run hydrate:model-data` from the repository root."</span>,</span><br><span class=line> );</span><br><span class=line> process.<span class=property>exitCode</span> = <span class=number>1</span>;</span><br><span class=line>}</span><br></pre></table></figure><p>实际校验不只是看目录存不存在,还会检查:<ul><li>provider JSON 文件是否齐全;<li><code>.manifest.json</code> 的 schema 版本;<li>模型目录结构哈希;<li>每个文件的 SHA-256;<li>模型的 <code>id</code>、<code>provider</code>、<code>api</code> 是否匹配;<li><code>baseUrl</code>、<code>reasoning</code>、输入模态、上下文窗口、最大输出和价格字段是否合法。</ul><p>看到下面的输出才说明这一步完成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>Generated model data is valid.</span><br></pre></table></figure><h2 id=三、配置模型和密钥><a class=headerlink href=#三、配置模型和密钥 title=三、配置模型和密钥></a>三、配置模型和密钥</h2><p>Pi 默认读取的全局目录是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>~/.pi/agent/</span><br></pre></table></figure><p>经常使用 Pi 的机器上一般已经有 <code>auth.json</code>、<code>models.json</code> 和相关设置,所以直接从源码启动时也能读取已有配置。<code>sdk.ts</code> 中创建模型运行时的代码如下:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br></pre><td class=code><pre><span class=line><span class=keyword>const</span> authPath = options.<span class=property>agentDir</span> ? <span class="title function_">join</span>(agentDir, <span class=string>"auth.json"</span>) : <span class=literal>undefined</span>;</span><br><span class=line><span class=keyword>const</span> modelsPath = options.<span class=property>agentDir</span> ? <span class="title function_">join</span>(agentDir, <span class=string>"models.json"</span>) : <span class=literal>undefined</span>;</span><br><span class=line><span class=keyword>const</span> modelRuntime =</span><br><span class=line> options.<span class=property>modelRuntime</span> ?? (<span class=keyword>await</span> <span class="title class_">ModelRuntime</span>.<span class="title function_">create</span>({ authPath, modelsPath }));</span><br></pre></table></figure><p>如果 <code>options.agentDir</code> 没有显式传入,<code>ModelRuntime.create()</code> 内部仍会回退到默认路径:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line><span class=keyword>const</span> modelsPath =</span><br><span class=line> options.<span class=property>modelsPath</span> === <span class=literal>null</span></span><br><span class=line> ? <span class=literal>undefined</span></span><br><span class=line> : (options.<span class=property>modelsPath</span> ?? <span class="title function_">join</span>(<span class="title function_">getAgentDir</span>(), <span class=string>"models.json"</span>));</span><br><span class=line><span class=keyword>const</span> config = <span class=keyword>await</span> <span class="title class_">ModelConfig</span>.<span class="title function_">load</span>(modelsPath);</span><br></pre></table></figure><h3 id=一个-OpenAI-兼容服务的配置例子><a title="一个 OpenAI 兼容服务的配置例子" class=headerlink href=#一个-OpenAI-兼容服务的配置例子></a>一个 OpenAI 兼容服务的配置例子</h3><p>在 <code>~/.pi/agent/models.json</code> 中可以增加:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"providers"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"my-openai-compatible"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"baseUrl"</span><span class=punctuation>:</span> <span class=string>"https://example.com/v1"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"api"</span><span class=punctuation>:</span> <span class=string>"openai-completions"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"apiKey"</span><span class=punctuation>:</span> <span class=string>"$PI_DEBUG_API_KEY"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"models"</span><span class=punctuation>:</span> <span class=punctuation>[</span></span><br><span class=line> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"id"</span><span class=punctuation>:</span> <span class=string>"my-model-id"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"name"</span><span class=punctuation>:</span> <span class=string>"My Debug Model"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"reasoning"</span><span class=punctuation>:</span> <span class=literal><span class=keyword>true</span></span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"input"</span><span class=punctuation>:</span> <span class=punctuation>[</span><span class=string>"text"</span><span class=punctuation>]</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"contextWindow"</span><span class=punctuation>:</span> <span class=number>128000</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"maxTokens"</span><span class=punctuation>:</span> <span class=number>16384</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"cost"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"input"</span><span class=punctuation>:</span> <span class=number>0</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"output"</span><span class=punctuation>:</span> <span class=number>0</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"cacheRead"</span><span class=punctuation>:</span> <span class=number>0</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"cacheWrite"</span><span class=punctuation>:</span> <span class=number>0</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>]</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p><code>apiKey</code> 支持直接写值、引用环境变量或执行命令。为了避免密钥进入 Git,调试时建议使用 <code>$PI_DEBUG_API_KEY</code> 这种环境变量引用。<h2 id=四、配置-VS-Code-调试入口><a title="四、配置 VS Code 调试入口" class=headerlink href=#四、配置-VS-Code-调试入口></a>四、配置 VS Code 调试入口</h2><p>源码入口是 <code>packages/coding-agent/src/cli.ts</code>。项目使用 TypeScript ESM,Node 通过 <code>tsx</code> 直接执行源码,因此一个单次请求的调试配置可以写成:<figure class="highlight json"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"version"</span><span class=punctuation>:</span> <span class=string>"0.2.0"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"configurations"</span><span class=punctuation>:</span> <span class=punctuation>[</span></span><br><span class=line> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"name"</span><span class=punctuation>:</span> <span class=string>"Pi:单次 Prompt,只读工具"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"type"</span><span class=punctuation>:</span> <span class=string>"node"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"request"</span><span class=punctuation>:</span> <span class=string>"launch"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"runtimeExecutable"</span><span class=punctuation>:</span> <span class=string>"node"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"runtimeArgs"</span><span class=punctuation>:</span> <span class=punctuation>[</span><span class=string>"--import"</span><span class=punctuation>,</span> <span class=string>"tsx"</span><span class=punctuation>]</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"program"</span><span class=punctuation>:</span> <span class=string>"${workspaceFolder}/packages/coding-agent/src/cli.ts"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"cwd"</span><span class=punctuation>:</span> <span class=string>"${workspaceFolder}"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"console"</span><span class=punctuation>:</span> <span class=string>"integratedTerminal"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"internalConsoleOptions"</span><span class=punctuation>:</span> <span class=string>"neverOpen"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"sourceMaps"</span><span class=punctuation>:</span> <span class=literal><span class=keyword>true</span></span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"smartStep"</span><span class=punctuation>:</span> <span class=literal><span class=keyword>true</span></span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"autoAttachChildProcesses"</span><span class=punctuation>:</span> <span class=literal><span class=keyword>true</span></span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"skipFiles"</span><span class=punctuation>:</span> <span class=punctuation>[</span></span><br><span class=line> <span class=string>"<node_internals>/**"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"${workspaceFolder}/node_modules/**"</span></span><br><span class=line> <span class=punctuation>]</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"args"</span><span class=punctuation>:</span> <span class=punctuation>[</span></span><br><span class=line> <span class=string>"--print"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"读取 package.json,并告诉我项目名称"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"--tools"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"read"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"--no-session"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"--no-extensions"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"--no-skills"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"--no-prompt-templates"</span><span class=punctuation>,</span></span><br><span class=line> <span class=string>"--no-context-files"</span></span><br><span class=line> <span class=punctuation>]</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"env"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"PI_DEBUG_API_KEY"</span><span class=punctuation>:</span> <span class=string>"${env:PI_DEBUG_API_KEY}"</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>}</span></span><br><span class=line> <span class=punctuation>]</span></span><br><span class=line><span class=punctuation>}</span></span><br></pre></table></figure><p>如果 VS Code 本身能继承终端中的环境变量,<code>env</code> 可以省略。macOS 上从图形界面启动 VS Code 时,经常读不到 shell 中设置的变量,这时可以显式增加 <code>env</code>,或者使用只保存在本机且被 <code>.gitignore</code> 排除的 <code>envFile</code>。<p>这里选择单次 Prompt 模式而不是一上来调 TUI,原因很实际:<ul><li>请求固定,方便反复复现;<li>只开放 <code>read</code>,不会因为模型输出而修改工作区;<li>禁用 session、extension、skill 和上下文文件后,调用链更短;<li>请求结束进程就退出,断点更容易观察。</ul><p>环境跑通以后,再切换到仓库自带的“Pi:交互模式”配置分析 steering、follow-up 和 TUI。<p>建议先在这些位置打断点:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br></pre><td class=code><pre><span class=line>packages/coding-agent/src/core/sdk.ts</span><br><span class=line> createAgentSession()</span><br><span class=line> new Agent({...})</span><br><span class=line> streamFn: async (...) => {...}</span><br><span class=line></span><br><span class=line>packages/coding-agent/src/core/agent-session.ts</span><br><span class=line> AgentSession.prompt()</span><br><span class=line> _runAgentPrompt()</span><br><span class=line></span><br><span class=line>packages/agent/src/agent.ts</span><br><span class=line> Agent.prompt()</span><br><span class=line> runPromptMessages()</span><br><span class=line> processEvents()</span><br><span class=line></span><br><span class=line>packages/agent/src/agent-loop.ts</span><br><span class=line> runAgentLoop()</span><br><span class=line> runLoop()</span><br><span class=line> streamAssistantResponse()</span><br><span class=line> executeToolCalls()</span><br></pre></table></figure><h2 id=五、一次请求的完整调用链><a class=headerlink href=#五、一次请求的完整调用链 title=五、一次请求的完整调用链></a>五、一次请求的完整调用链</h2><p>先给出总链路,后面再逐层展开:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br></pre><td class=code><pre><span class=line>packages/coding-agent/src/cli.ts</span><br><span class=line> └─ main(process.argv.slice(2))</span><br><span class=line> ├─ createAgentSessionRuntime(...)</span><br><span class=line> │ └─ createAgentSession(...)</span><br><span class=line> │ ├─ ModelRuntime.create(...)</span><br><span class=line> │ └─ new Agent({ streamFn, convertToLlm, ... })</span><br><span class=line> └─ runPrintMode(...) / InteractiveMode.run()</span><br><span class=line> └─ AgentSession.prompt(...)</span><br><span class=line> └─ Agent.prompt(...)</span><br><span class=line> └─ runAgentLoop(...)</span><br><span class=line> └─ runLoop(...)</span><br><span class=line> ├─ streamAssistantResponse(...)</span><br><span class=line> │ └─ streamFunction(...)</span><br><span class=line> │ └─ ModelRuntime.streamSimple(...)</span><br><span class=line> │ └─ provider.streamSimple(...)</span><br><span class=line> └─ executeToolCalls(...)</span><br><span class=line> └─ tool.execute(...)</span><br></pre></table></figure><p>CLI 入口本身很薄:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line><span class=meta>#!/usr/bin/env node</span></span><br><span class=line><span class=keyword>import</span> { setupCli } <span class=keyword>from</span> <span class=string>"./cli/setup.ts"</span>;</span><br><span class=line><span class=keyword>import</span> { main } <span class=keyword>from</span> <span class=string>"./main.ts"</span>;</span><br><span class=line></span><br><span class=line><span class="title function_">setupCli</span>();</span><br><span class=line><span class="title function_">main</span>(process.<span class=property>argv</span>.<span class="title function_">slice</span>(<span class=number>2</span>));</span><br></pre></table></figure><p>单次 Prompt 模式最后调用 <code>runPrintMode()</code>,里面再进入 <code>AgentSession</code>:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line><span class=keyword>if</span> (initialMessage) {</span><br><span class=line> <span class=keyword>await</span> session.<span class="title function_">prompt</span>(initialMessage, { <span class=attr>images</span>: initialImages });</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>for</span> (<span class=keyword>const</span> message <span class=keyword>of</span> messages) {</span><br><span class=line> <span class=keyword>await</span> session.<span class="title function_">prompt</span>(message);</span><br><span class=line>}</span><br></pre></table></figure><h2 id=六、Agent-初始化:streamFn-在-sdk-ts-中注入><a title="六、Agent 初始化:streamFn 在 sdk.ts 中注入" class=headerlink href=#六、Agent-初始化:streamFn-在-sdk-ts-中注入></a>六、Agent 初始化:streamFn 在 sdk.ts 中注入</h2><h3 id=1-先安装一个兼容性默认值><a title="1. 先安装一个兼容性默认值" class=headerlink href=#1-先安装一个兼容性默认值></a>1. 先安装一个兼容性默认值</h3><p><code>packages/coding-agent/src/core/sdk.ts</code> 文件顶部有:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br></pre><td class=code><pre><span class=line><span class=keyword>import</span> {</span><br><span class=line> <span class="title class_">Agent</span>,</span><br><span class=line> <span class=keyword>type</span> <span class="title class_">AgentMessage</span>,</span><br><span class=line> setDefaultStreamFn,</span><br><span class=line> <span class=keyword>type</span> <span class="title class_">ThinkingLevel</span>,</span><br><span class=line>} <span class=keyword>from</span> <span class=string>"@earendil-works/pi-agent-core"</span>;</span><br><span class=line><span class=keyword>import</span> {</span><br><span class=line> clampThinkingLevel,</span><br><span class=line> <span class=keyword>type</span> <span class="title class_">Message</span>,</span><br><span class=line> <span class=keyword>type</span> <span class="title class_">Model</span>,</span><br><span class=line> streamSimple,</span><br><span class=line>} <span class=keyword>from</span> <span class=string>"@earendil-works/pi-ai/compat"</span>;</span><br><span class=line></span><br><span class=line><span class=comment>// Preserve the pre-0.81 fallback for extensions that construct Agent instances</span></span><br><span class=line><span class=comment>// or invoke low-level agent loops without supplying streamFn. Agent core remains</span></span><br><span class=line><span class=comment>// provider-agnostic and does not import pi-ai/compat itself.</span></span><br><span class=line><span class="title function_">setDefaultStreamFn</span>(streamSimple);</span><br></pre></table></figure><p>这一步是兼容旧扩展的全局 fallback。Agent Core 自己不依赖 <code>pi-ai/compat</code>,由 Coding Agent 在启动时把默认流函数注册进去。<p><code>packages/agent/src/stream-fn.ts</code> 的全部核心逻辑如下:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br></pre><td class=code><pre><span class=line><span class=keyword>import</span> <span class=keyword>type</span> { <span class="title class_">StreamFn</span> } <span class=keyword>from</span> <span class=string>"./types.ts"</span>;</span><br><span class=line></span><br><span class=line><span class=keyword>let</span> <span class=attr>defaultStreamFn</span>: <span class="title class_">StreamFn</span> | <span class=literal>undefined</span>;</span><br><span class=line></span><br><span class=line><span class=keyword>export</span> <span class=keyword>function</span> <span class="title function_">setDefaultStreamFn</span>(<span class=params>streamFn: StreamFn | <span class=literal>undefined</span></span>): <span class=built_in>void</span> {</span><br><span class=line> defaultStreamFn = streamFn;</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>export</span> <span class=keyword>function</span> <span class="title function_">getDefaultStreamFn</span>(<span class=params></span>): <span class="title class_">StreamFn</span> {</span><br><span class=line> <span class=keyword>if</span> (!defaultStreamFn) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">Error</span>(</span><br><span class=line> <span class=string>"No default stream function configured. "</span> +</span><br><span class=line> <span class=string>"Pass streamFn explicitly or call setDefaultStreamFn()."</span>,</span><br><span class=line> );</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> defaultStreamFn;</span><br><span class=line>}</span><br></pre></table></figure><h3 id=2-实际-Agent-使用的是显式注入的-streamFn><a title="2. 实际 Agent 使用的是显式注入的 streamFn" class=headerlink href=#2-实际-Agent-使用的是显式注入的-streamFn></a>2. 实际 Agent 使用的是显式注入的 streamFn</h3><p>创建会话时,<code>sdk.ts</code> 并不是只依赖上面的 fallback,而是给 <code>new Agent()</code> 显式传入一个包装函数:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br><span class=line>50</span><br><span class=line>51</span><br><span class=line>52</span><br><span class=line>53</span><br><span class=line>54</span><br><span class=line>55</span><br><span class=line>56</span><br><span class=line>57</span><br><span class=line>58</span><br><span class=line>59</span><br><span class=line>60</span><br><span class=line>61</span><br><span class=line>62</span><br><span class=line>63</span><br><span class=line>64</span><br><span class=line>65</span><br><span class=line>66</span><br><span class=line>67</span><br><span class=line>68</span><br><span class=line>69</span><br><span class=line>70</span><br><span class=line>71</span><br><span class=line>72</span><br><span class=line>73</span><br><span class=line>74</span><br><span class=line>75</span><br><span class=line>76</span><br><span class=line>77</span><br><span class=line>78</span><br><span class=line>79</span><br><span class=line>80</span><br><span class=line>81</span><br><span class=line>82</span><br><span class=line>83</span><br><span class=line>84</span><br><span class=line>85</span><br><span class=line>86</span><br><span class=line>87</span><br><span class=line>88</span><br><span class=line>89</span><br><span class=line>90</span><br><span class=line>91</span><br><span class=line>92</span><br><span class=line>93</span><br><span class=line>94</span><br><span class=line>95</span><br><span class=line>96</span><br><span class=line>97</span><br><span class=line>98</span><br><span class=line>99</span><br><span class=line>100</span><br><span class=line>101</span><br><span class=line>102</span><br><span class=line>103</span><br><span class=line>104</span><br><span class=line>105</span><br><span class=line>106</span><br><span class=line>107</span><br><span class=line>108</span><br><span class=line>109</span><br><span class=line>110</span><br><span class=line>111</span><br><span class=line>112</span><br><span class=line>113</span><br><span class=line>114</span><br></pre><td class=code><pre><span class=line>agent = <span class=keyword>new</span> <span class="title class_">Agent</span>({</span><br><span class=line> <span class=comment>// initialState 只是 Agent Core 的初始快照。</span></span><br><span class=line> <span class=comment>// systemPrompt 和 tools 暂时为空,AgentSession 随后会根据资源加载结果填充。</span></span><br><span class=line> <span class=attr>initialState</span>: {</span><br><span class=line> <span class=attr>systemPrompt</span>: <span class=string>""</span>,</span><br><span class=line> model,</span><br><span class=line> thinkingLevel,</span><br><span class=line> <span class=attr>tools</span>: [],</span><br><span class=line> },</span><br><span class=line></span><br><span class=line> <span class=comment>// Coding Agent 中除了 user/assistant/toolResult,还有扩展产生的 custom message。</span></span><br><span class=line> <span class=comment>// 这里负责把产品层消息转成 pi-ai 接受的 Message,并按设置屏蔽图片。</span></span><br><span class=line> <span class=attr>convertToLlm</span>: convertToLlmWithBlockImages,</span><br><span class=line></span><br><span class=line> <span class=comment>// 这是本文最关键的注入点。</span></span><br><span class=line> <span class=comment>// Agent Core 只保存此函数的引用,不知道认证文件、供应商 SDK 和重试配置。</span></span><br><span class=line> <span class=attr>streamFn</span>: <span class=keyword>async</span> (model, context, options) => {</span><br><span class=line> <span class=comment>// 每次请求时动态读取设置,所以运行中修改配置也能影响后续 turn。</span></span><br><span class=line> <span class=keyword>const</span> providerRetrySettings = settingsManager.<span class="title function_">getProviderRetrySettings</span>();</span><br><span class=line> <span class=keyword>const</span> httpIdleTimeoutMs = settingsManager.<span class="title function_">getHttpIdleTimeoutMs</span>();</span><br><span class=line></span><br><span class=line> <span class=comment>// SDKs treat timeout=0 as 0ms (immediate timeout), not "no timeout".</span></span><br><span class=line> <span class=comment>// Use max int32 to effectively disable the timeout.</span></span><br><span class=line> <span class=keyword>const</span> effectiveTimeoutMs =</span><br><span class=line> httpIdleTimeoutMs === <span class=number>0</span> ? <span class=number>2147483647</span> : httpIdleTimeoutMs;</span><br><span class=line></span><br><span class=line> <span class=comment>// 优先级:单次调用参数 > provider 重试配置 > 全局 HTTP 空闲超时。</span></span><br><span class=line> <span class=keyword>const</span> timeoutMs =</span><br><span class=line> options?.<span class=property>timeoutMs</span> ??</span><br><span class=line> providerRetrySettings.<span class=property>timeoutMs</span> ??</span><br><span class=line> effectiveTimeoutMs;</span><br><span class=line></span><br><span class=line> <span class=comment>// WebSocket 连接超时独立于普通 HTTP 空闲超时。</span></span><br><span class=line> <span class=keyword>const</span> websocketConnectTimeoutMs =</span><br><span class=line> options?.<span class=property>websocketConnectTimeoutMs</span> ??</span><br><span class=line> settingsManager.<span class="title function_">getWebSocketConnectTimeoutMs</span>();</span><br><span class=line></span><br><span class=line> <span class=comment>// extensionRunnerRef 在 Agent 创建后由 AgentSession 绑定。</span></span><br><span class=line> <span class=comment>// 用 ref 是为了让闭包始终拿到当前 runner,而不是初始化时的 undefined。</span></span><br><span class=line> <span class=keyword>const</span> headerRunner = extensionRunnerRef.<span class=property>current</span>;</span><br><span class=line></span><br><span class=line> <span class=comment>// 真正进入模型运行时。ModelRuntime 会解析 provider、认证与协议实现。</span></span><br><span class=line> <span class=keyword>return</span> modelRuntime.<span class="title function_">streamSimple</span>(model, context, {</span><br><span class=line> <span class=comment>// 先保留 Agent Core 传来的 signal、apiKey、thinking 等请求选项。</span></span><br><span class=line> ...options,</span><br><span class=line> timeoutMs,</span><br><span class=line> websocketConnectTimeoutMs,</span><br><span class=line></span><br><span class=line> <span class=comment>// 单次请求可覆盖默认重试策略;未传入才读取 SettingsManager。</span></span><br><span class=line> <span class=attr>maxRetries</span>:</span><br><span class=line> options?.<span class=property>maxRetries</span> ?? providerRetrySettings.<span class=property>maxRetries</span>,</span><br><span class=line> <span class=attr>maxRetryDelayMs</span>:</span><br><span class=line> options?.<span class=property>maxRetryDelayMs</span> ??</span><br><span class=line> providerRetrySettings.<span class=property>maxRetryDelayMs</span>,</span><br><span class=line></span><br><span class=line> <span class=comment>// provider 即将发送请求前执行。先补 Pi 的来源/session header,</span></span><br><span class=line> <span class=comment>// 再让扩展做最后修改,扩展因此拥有最终 header 的控制权。</span></span><br><span class=line> <span class=attr>transformHeaders</span>: <span class=keyword>async</span> (requestHeaders) => {</span><br><span class=line> <span class=keyword>const</span> headers = <span class="title function_">mergeProviderAttributionHeaders</span>(</span><br><span class=line> model,</span><br><span class=line> settingsManager,</span><br><span class=line> options?.<span class=property>sessionId</span>,</span><br><span class=line> requestHeaders,</span><br><span class=line> );</span><br><span class=line> <span class=keyword>return</span> headerRunner?.<span class="title function_">hasHandlers</span>(<span class=string>"before_provider_headers"</span>)</span><br><span class=line> ? headerRunner.<span class="title function_">emitBeforeProviderHeaders</span>(headers ?? {})</span><br><span class=line> : (headers ?? {});</span><br><span class=line> },</span><br><span class=line> });</span><br><span class=line> },</span><br><span class=line></span><br><span class=line> <span class=comment>// onPayload 处理已经转换成供应商协议、但尚未真正发出的请求体。</span></span><br><span class=line> <span class=comment>// 没有扩展监听时直接原样返回,避免无意义复制。</span></span><br><span class=line> <span class=attr>onPayload</span>: <span class=keyword>async</span> (payload, _model) => {</span><br><span class=line> <span class=keyword>const</span> runner = extensionRunnerRef.<span class=property>current</span>;</span><br><span class=line> <span class=keyword>if</span> (!runner?.<span class="title function_">hasHandlers</span>(<span class=string>"before_provider_request"</span>)) {</span><br><span class=line> <span class=keyword>return</span> payload;</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> runner.<span class="title function_">emitBeforeProviderRequest</span>(payload);</span><br><span class=line> },</span><br><span class=line></span><br><span class=line> <span class=comment>// onResponse 暴露 HTTP 状态与响应头,而正文仍由流事件处理。</span></span><br><span class=line> <span class=attr>onResponse</span>: <span class=keyword>async</span> (response, _model) => {</span><br><span class=line> <span class=keyword>const</span> runner = extensionRunnerRef.<span class=property>current</span>;</span><br><span class=line> <span class=keyword>if</span> (!runner?.<span class="title function_">hasHandlers</span>(<span class=string>"after_provider_response"</span>)) {</span><br><span class=line> <span class=keyword>return</span>;</span><br><span class=line> }</span><br><span class=line> <span class=keyword>await</span> runner.<span class="title function_">emit</span>({</span><br><span class=line> <span class=attr>type</span>: <span class=string>"after_provider_response"</span>,</span><br><span class=line> <span class=attr>status</span>: response.<span class=property>status</span>,</span><br><span class=line> <span class=attr>headers</span>: response.<span class=property>headers</span>,</span><br><span class=line> });</span><br><span class=line> },</span><br><span class=line></span><br><span class=line> <span class=comment>// sessionId 会一路传到 provider,可供支持会话亲和或缓存的后端使用。</span></span><br><span class=line> <span class=attr>sessionId</span>: sessionManager.<span class="title function_">getSessionId</span>(),</span><br><span class=line></span><br><span class=line> <span class=comment>// 在 convertToLlm 之前运行扩展的上下文 hook。</span></span><br><span class=line> <span class=comment>// 它操作的仍然是 AgentMessage[],适合裁剪、插入或改写上下文。</span></span><br><span class=line> <span class=attr>transformContext</span>: <span class=keyword>async</span> (messages) => {</span><br><span class=line> <span class=keyword>const</span> runner = extensionRunnerRef.<span class=property>current</span>;</span><br><span class=line> <span class=keyword>if</span> (!runner) <span class=keyword>return</span> messages;</span><br><span class=line> <span class=keyword>return</span> runner.<span class="title function_">emitContext</span>(messages);</span><br><span class=line> },</span><br><span class=line></span><br><span class=line> <span class=comment>// steering 选择何时向进行中的任务插话;followUp 决定后续任务如何出队。</span></span><br><span class=line> <span class=attr>steeringMode</span>: settingsManager.<span class="title function_">getSteeringMode</span>(),</span><br><span class=line> <span class=attr>followUpMode</span>: settingsManager.<span class="title function_">getFollowUpMode</span>(),</span><br><span class=line></span><br><span class=line> <span class=comment>// 这些值不会在 sdk.ts 中消费,而是继续透传给 Agent Loop 和 provider。</span></span><br><span class=line> <span class=attr>transport</span>: settingsManager.<span class="title function_">getTransport</span>(),</span><br><span class=line> <span class=attr>thinkingBudgets</span>: settingsManager.<span class="title function_">getThinkingBudgets</span>(),</span><br><span class=line> <span class=attr>maxRetryDelayMs</span>: settingsManager.<span class="title function_">getProviderRetrySettings</span>().<span class=property>maxRetryDelayMs</span>,</span><br><span class=line>});</span><br></pre></table></figure><p>这个包装函数不只是把请求转发出去,它还在真正请求前补上:<ul><li>HTTP 空闲超时和 WebSocket 连接超时;<li>最大重试次数、最大重试等待时间;<li>provider attribution headers;<li>扩展提供的 <code>before_provider_headers</code> hook;<li>请求 payload 与响应 headers 的扩展 hook。</ul><p>因此 <code>streamFn</code> 是 Agent Core 与真实模型运行时之间的注入点,也是调试“最终请求为什么长这样”的最佳断点之一。<h3 id=3-StreamFn-的契约><a title="3. StreamFn 的契约" class=headerlink href=#3-StreamFn-的契约></a>3. StreamFn 的契约</h3><p>它不是一个普通的 <code>Promise<AssistantMessage></code>,而是返回事件流:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line><span class=keyword>export</span> <span class=keyword>type</span> <span class="title class_">StreamFn</span> = <span class=function>(<span class=params></span></span></span><br><span class=line><span class=params><span class=function> model: Model<Api>,</span></span></span><br><span class=line><span class=params><span class=function> context: Context,</span></span></span><br><span class=line><span class=params><span class=function> options?: SimpleStreamOptions,</span></span></span><br><span class=line><span class=params><span class=function></span>) =></span> <span class="title class_">AssistantMessageEventStream</span> | <span class="title class_">Promise</span><<span class="title class_">AssistantMessageEventStream</span>>;</span><br></pre></table></figure><p>源码注释还规定,请求失败不应该直接 <code>throw</code>,而要在流中产生错误事件,并以 <code>stopReason: "error"</code> 或 <code>"aborted"</code> 的最终 AssistantMessage 收尾。这样上层可以用统一的事件协议处理正常完成、失败和取消。<h3 id=4-ModelRuntime-再选择具体-Provider><a title="4. ModelRuntime 再选择具体 Provider" class=headerlink href=#4-ModelRuntime-再选择具体-Provider></a>4. ModelRuntime 再选择具体 Provider</h3><p><code>sdk.ts</code> 注入的函数最终来到 <code>ModelRuntime.streamSimple()</code>:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br></pre><td class=code><pre><span class=line><span class="title function_">streamSimple</span>(</span><br><span class=line> <span class=attr>model</span>: <span class="title class_">Model</span><<span class="title class_">Api</span>>,</span><br><span class=line> <span class=attr>context</span>: <span class="title class_">Context</span>,</span><br><span class=line> options?: <span class="title class_">ModelsSimpleStreamOptions</span>,</span><br><span class=line>): <span class="title class_">AssistantMessageEventStream</span> {</span><br><span class=line> <span class=keyword>return</span> <span class="title function_">lazyStream</span>(model, <span class=keyword>async</span> () => {</span><br><span class=line> <span class=keyword>const</span> prepared = <span class=keyword>await</span> <span class="variable language_">this</span>.<span class="title function_">prepareRequest</span>(model, options);</span><br><span class=line> <span class=keyword>return</span> prepared.<span class=property>provider</span>.<span class="title function_">streamSimple</span>(</span><br><span class=line> prepared.<span class=property>model</span>,</span><br><span class=line> context,</span><br><span class=line> prepared.<span class=property>options</span> <span class=keyword>as</span> <span class="title class_">SimpleStreamOptions</span>,</span><br><span class=line> );</span><br><span class=line> });</span><br><span class=line>}</span><br></pre></table></figure><p>到这里才根据 model 找到对应 provider,准备认证和请求参数,然后调用各 provider 的 <code>streamSimple</code>。<code>agent-loop.ts</code> 完全不用知道下层究竟是 OpenAI Responses、OpenAI Completions、Anthropic Messages 还是 Google Generative AI。<h2 id=七、从-AgentSession-进入-Agent-Core><a title="七、从 AgentSession 进入 Agent Core" class=headerlink href=#七、从-AgentSession-进入-Agent-Core></a>七、从 AgentSession 进入 Agent Core</h2><p><code>AgentSession.prompt()</code> 在进入 Core 前会处理很多产品层能力,包括扩展命令、Prompt 模板、Skill、模型与认证检查、压缩检查以及扩展 hook。最后构造用户消息并调用:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br></pre><td class=code><pre><span class=line><span class=keyword>private</span> <span class=keyword>async</span> <span class="title function_">_runAgentPrompt</span>(</span><br><span class=line> <span class=attr>messages</span>: <span class="title class_">AgentMessage</span> | <span class="title class_">AgentMessage</span>[],</span><br><span class=line>): <span class="title class_">Promise</span><<span class=built_in>void</span>> {</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_isAgentRunActive</span> = <span class=literal>true</span>;</span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> <span class=keyword>await</span> <span class="variable language_">this</span>.<span class=property>agent</span>.<span class="title function_">prompt</span>(messages);</span><br><span class=line> <span class=keyword>while</span> (<span class=keyword>await</span> <span class="variable language_">this</span>.<span class="title function_">_handlePostAgentRun</span>()) {</span><br><span class=line> <span class=keyword>await</span> <span class="variable language_">this</span>.<span class=property>agent</span>.<span class="title function_">continue</span>();</span><br><span class=line> }</span><br><span class=line> } <span class=keyword>finally</span> {</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_systemPromptOverride</span> = <span class=literal>undefined</span>;</span><br><span class=line> <span class="variable language_">this</span>.<span class="title function_">_flushPendingBashMessages</span>();</span><br><span class=line> <span class="variable language_">this</span>.<span class="title function_">_flushPendingCustomMessages</span>();</span><br><span class=line> <span class=keyword>await</span> <span class="variable language_">this</span>.<span class="title function_">_emitAgentSettled</span>();</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><p>这里还有一层“运行结束后的继续处理”,主要服务于 Coding Agent 会话层的压缩等逻辑。不要把它和 <code>agent-loop.ts</code> 内部的双层循环混在一起。<p>进入 <code>Agent</code> 后,字符串会先被规范化为 <code>AgentMessage[]</code>,然后调用 <code>runAgentLoop()</code>:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br></pre><td class=code><pre><span class=line><span class=keyword>async</span> <span class="title function_">prompt</span>(<span class=attr>message</span>: <span class="title class_">AgentMessage</span> | <span class="title class_">AgentMessage</span>[]): <span class="title class_">Promise</span><<span class=built_in>void</span>>;</span><br><span class=line><span class=keyword>async</span> <span class="title function_">prompt</span>(<span class=attr>input</span>: <span class=built_in>string</span>, images?: <span class="title class_">ImageContent</span>[]): <span class="title class_">Promise</span><<span class=built_in>void</span>>;</span><br><span class=line><span class=keyword>async</span> <span class="title function_">prompt</span>(</span><br><span class=line> <span class=attr>input</span>: <span class=built_in>string</span> | <span class="title class_">AgentMessage</span> | <span class="title class_">AgentMessage</span>[],</span><br><span class=line> images?: <span class="title class_">ImageContent</span>[],</span><br><span class=line>): <span class="title class_">Promise</span><<span class=built_in>void</span>> {</span><br><span class=line> <span class=comment>// activeRun 同时充当并发保护:一个 Agent 实例不能并行跑两个 prompt。</span></span><br><span class=line> <span class=keyword>if</span> (<span class="variable language_">this</span>.<span class=property>activeRun</span>) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">Error</span>(</span><br><span class=line> <span class=string>"Agent is already processing a prompt. Use steer() or followUp() "</span> +</span><br><span class=line> <span class=string>"to queue messages, or wait for completion."</span>,</span><br><span class=line> );</span><br><span class=line> }</span><br><span class=line> <span class=comment>// string、单个 AgentMessage 和 AgentMessage[] 最终统一成数组。</span></span><br><span class=line> <span class=comment>// 字符串会被包装为 role=user,并附加 timestamp 与可选图片。</span></span><br><span class=line> <span class=keyword>const</span> messages = <span class="variable language_">this</span>.<span class="title function_">normalizePromptInput</span>(input, images);</span><br><span class=line> <span class=keyword>await</span> <span class="variable language_">this</span>.<span class="title function_">runPromptMessages</span>(messages);</span><br><span class=line>}</span><br><span class=line></span><br><span class=line><span class=keyword>private</span> <span class=keyword>async</span> <span class="title function_">runPromptMessages</span>(</span><br><span class=line> <span class=attr>messages</span>: <span class="title class_">AgentMessage</span>[],</span><br><span class=line> <span class=attr>options</span>: { skipInitialSteeringPoll?: <span class=built_in>boolean</span> } = {},</span><br><span class=line>): <span class="title class_">Promise</span><<span class=built_in>void</span>> {</span><br><span class=line> <span class=comment>// runWithLifecycle 创建 AbortController、设置 isStreaming,</span></span><br><span class=line> <span class=comment>// 并保证成功、异常或取消后都能清理 activeRun。</span></span><br><span class=line> <span class=keyword>await</span> <span class="variable language_">this</span>.<span class="title function_">runWithLifecycle</span>(<span class=keyword>async</span> (signal) => {</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">runAgentLoop</span>(</span><br><span class=line> <span class=comment>// 本次新输入;runAgentLoop 会把它拼到历史上下文末尾。</span></span><br><span class=line> messages,</span><br><span class=line> <span class=comment>// 使用数组副本,避免低层循环直接修改 Agent.state 的顶层数组。</span></span><br><span class=line> <span class="variable language_">this</span>.<span class="title function_">createContextSnapshot</span>(),</span><br><span class=line> <span class=comment>// 把模型、hook、队列读取器和执行策略冻结为本次运行配置。</span></span><br><span class=line> <span class="variable language_">this</span>.<span class="title function_">createLoopConfig</span>(options),</span><br><span class=line> <span class=comment>// 主循环只 emit;Agent 通过 processEvents 归并自己的状态。</span></span><br><span class=line> <span class=function>(<span class=params>event</span>) =></span> <span class="variable language_">this</span>.<span class="title function_">processEvents</span>(event),</span><br><span class=line> <span class=comment>// 同一个 AbortSignal 贯穿模型请求、hook 与工具调用。</span></span><br><span class=line> signal,</span><br><span class=line> <span class=comment>// 即 sdk.ts 构造 Agent 时显式注入的 streamFn。</span></span><br><span class=line> <span class="variable language_">this</span>.<span class=property>streamFunction</span>,</span><br><span class=line> );</span><br><span class=line> });</span><br><span class=line>}</span><br></pre></table></figure><p>注意最后一个参数 <code>this.streamFunction</code>,它就是 <code>sdk.ts</code> 创建 Agent 时传进来的函数。到这里,初始化阶段的依赖注入和运行阶段的实际调用闭环了。<h2 id=八、runAgentLoop:准备上下文和事件><a class=headerlink href=#八、runAgentLoop:准备上下文和事件 title=八、runAgentLoop:准备上下文和事件></a>八、runAgentLoop:准备上下文和事件</h2><p><code>runAgentLoop()</code> 没有立刻发请求,而是先复制上下文、发出生命周期事件:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br></pre><td class=code><pre><span class=line><span class=keyword>export</span> <span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">runAgentLoop</span>(<span class=params></span></span><br><span class=line><span class=params> prompts: AgentMessage[],</span></span><br><span class=line><span class=params> context: AgentContext,</span></span><br><span class=line><span class=params> config: AgentLoopConfig,</span></span><br><span class=line><span class=params> emit: AgentEventSink,</span></span><br><span class=line><span class=params> signal: AbortSignal | <span class=literal>undefined</span>,</span></span><br><span class=line><span class=params> streamFn: StreamFn,</span></span><br><span class=line><span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">AgentMessage</span>[]> {</span><br><span class=line> <span class=comment>// newMessages 是本次运行的增量结果,初始就包含用户新提交的 prompts。</span></span><br><span class=line> <span class=keyword>const</span> <span class=attr>newMessages</span>: <span class="title class_">AgentMessage</span>[] = [...prompts];</span><br><span class=line></span><br><span class=line> <span class=comment>// currentContext 是本次循环使用的工作副本:保留旧历史,再拼接新输入。</span></span><br><span class=line> <span class=comment>// 展开 context 只浅复制对象,因此 messages 必须单独创建新数组。</span></span><br><span class=line> <span class=keyword>const</span> <span class=attr>currentContext</span>: <span class="title class_">AgentContext</span> = {</span><br><span class=line> ...context,</span><br><span class=line> <span class=attr>messages</span>: [...context.<span class=property>messages</span>, ...prompts],</span><br><span class=line> };</span><br><span class=line></span><br><span class=line> <span class=comment>// 事件顺序是协议的一部分:先开始 Agent 和 turn,再完整发布用户消息。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"agent_start"</span> });</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"turn_start"</span> });</span><br><span class=line> <span class=keyword>for</span> (<span class=keyword>const</span> prompt <span class=keyword>of</span> prompts) {</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"message_start"</span>, <span class=attr>message</span>: prompt });</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"message_end"</span>, <span class=attr>message</span>: prompt });</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 正常的 Coding Agent 总会显式传 streamFn;右侧是旧调用方的兼容 fallback。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">runLoop</span>(</span><br><span class=line> currentContext,</span><br><span class=line> newMessages,</span><br><span class=line> config,</span><br><span class=line> signal,</span><br><span class=line> emit,</span><br><span class=line> streamFn ?? <span class="title function_">getDefaultStreamFn</span>(),</span><br><span class=line> );</span><br><span class=line> <span class=comment>// runLoop 内部通过 push 修改 newMessages,这里返回的就是本次运行完整增量。</span></span><br><span class=line> <span class=keyword>return</span> newMessages;</span><br><span class=line>}</span><br></pre></table></figure><p>这里有两个消息集合:<ul><li><code>currentContext.messages</code> 是发给模型的完整上下文;<li><code>newMessages</code> 只记录本次 Agent Run 新产生的消息,最后随 <code>agent_end</code> 返回。</ul><p>这两个集合在循环中同时追加,但语义不同。调试时如果只盯一个数组,很容易误以为消息被重复添加。<h2 id=九、核心:两层-Agent-Loop-到底在循环什么><a title="九、核心:两层 Agent Loop 到底在循环什么" class=headerlink href=#九、核心:两层-Agent-Loop-到底在循环什么></a>九、核心:两层 Agent Loop 到底在循环什么</h2><p>下面是 <code>runLoop()</code> 的核心代码。片段较长,但只有结合真实代码才能看清两个循环的退出条件:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br><span class=line>50</span><br><span class=line>51</span><br><span class=line>52</span><br><span class=line>53</span><br><span class=line>54</span><br><span class=line>55</span><br><span class=line>56</span><br><span class=line>57</span><br><span class=line>58</span><br><span class=line>59</span><br><span class=line>60</span><br><span class=line>61</span><br><span class=line>62</span><br><span class=line>63</span><br><span class=line>64</span><br><span class=line>65</span><br><span class=line>66</span><br><span class=line>67</span><br><span class=line>68</span><br><span class=line>69</span><br><span class=line>70</span><br><span class=line>71</span><br><span class=line>72</span><br><span class=line>73</span><br><span class=line>74</span><br><span class=line>75</span><br><span class=line>76</span><br><span class=line>77</span><br><span class=line>78</span><br><span class=line>79</span><br><span class=line>80</span><br><span class=line>81</span><br><span class=line>82</span><br><span class=line>83</span><br><span class=line>84</span><br><span class=line>85</span><br><span class=line>86</span><br><span class=line>87</span><br><span class=line>88</span><br><span class=line>89</span><br><span class=line>90</span><br><span class=line>91</span><br><span class=line>92</span><br><span class=line>93</span><br><span class=line>94</span><br><span class=line>95</span><br><span class=line>96</span><br><span class=line>97</span><br><span class=line>98</span><br><span class=line>99</span><br><span class=line>100</span><br><span class=line>101</span><br><span class=line>102</span><br><span class=line>103</span><br><span class=line>104</span><br><span class=line>105</span><br><span class=line>106</span><br><span class=line>107</span><br><span class=line>108</span><br><span class=line>109</span><br><span class=line>110</span><br><span class=line>111</span><br><span class=line>112</span><br><span class=line>113</span><br><span class=line>114</span><br><span class=line>115</span><br><span class=line>116</span><br><span class=line>117</span><br><span class=line>118</span><br><span class=line>119</span><br><span class=line>120</span><br><span class=line>121</span><br><span class=line>122</span><br><span class=line>123</span><br><span class=line>124</span><br><span class=line>125</span><br><span class=line>126</span><br><span class=line>127</span><br><span class=line>128</span><br><span class=line>129</span><br><span class=line>130</span><br><span class=line>131</span><br><span class=line>132</span><br><span class=line>133</span><br><span class=line>134</span><br><span class=line>135</span><br><span class=line>136</span><br><span class=line>137</span><br><span class=line>138</span><br><span class=line>139</span><br><span class=line>140</span><br><span class=line>141</span><br><span class=line>142</span><br><span class=line>143</span><br><span class=line>144</span><br><span class=line>145</span><br><span class=line>146</span><br><span class=line>147</span><br><span class=line>148</span><br><span class=line>149</span><br><span class=line>150</span><br><span class=line>151</span><br><span class=line>152</span><br><span class=line>153</span><br><span class=line>154</span><br><span class=line>155</span><br><span class=line>156</span><br><span class=line>157</span><br><span class=line>158</span><br><span class=line>159</span><br><span class=line>160</span><br><span class=line>161</span><br><span class=line>162</span><br><span class=line>163</span><br><span class=line>164</span><br><span class=line>165</span><br><span class=line>166</span><br><span class=line>167</span><br><span class=line>168</span><br><span class=line>169</span><br><span class=line>170</span><br><span class=line>171</span><br><span class=line>172</span><br><span class=line>173</span><br><span class=line>174</span><br><span class=line>175</span><br><span class=line>176</span><br><span class=line>177</span><br><span class=line>178</span><br></pre><td class=code><pre><span class=line><span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">runLoop</span>(<span class=params></span></span><br><span class=line><span class=params> initialContext: AgentContext,</span></span><br><span class=line><span class=params> newMessages: AgentMessage[],</span></span><br><span class=line><span class=params> initialConfig: AgentLoopConfig,</span></span><br><span class=line><span class=params> signal: AbortSignal | <span class=literal>undefined</span>,</span></span><br><span class=line><span class=params> emit: AgentEventSink,</span></span><br><span class=line><span class=params> streamFunction: StreamFn,</span></span><br><span class=line><span class=params></span>): <span class="title class_">Promise</span><<span class=built_in>void</span>> {</span><br><span class=line> <span class=comment>// 这两个变量都可能被 prepareNextTurn 替换,所以不能使用 const。</span></span><br><span class=line> <span class=keyword>let</span> currentContext = initialContext;</span><br><span class=line> <span class=keyword>let</span> config = initialConfig;</span><br><span class=line> <span class=comment>// 第一轮之前没有已完成的 turn;从第二轮起它携带上一轮快照。</span></span><br><span class=line> <span class=keyword>let</span> <span class=attr>lastCompletedTurn</span>: <span class="title class_">PrepareNextTurnContext</span> | <span class=literal>undefined</span>;</span><br><span class=line></span><br><span class=line> <span class=comment>// 进入循环前先拉取一次 steering。例如 Agent 真正开始执行前,</span></span><br><span class=line> <span class=comment>// 用户已经排入了一条纠偏消息,它也应进入第一次请求。</span></span><br><span class=line> <span class=keyword>let</span> <span class=attr>pendingMessages</span>: <span class="title class_">AgentMessage</span>[] =</span><br><span class=line> (<span class=keyword>await</span> config.<span class=property>getSteeringMessages</span>?.()) || [];</span><br><span class=line></span><br><span class=line> <span class=comment>// 外层:Agent 本来要停下时,如果还有 follow-up,就重新进入内层</span></span><br><span class=line> <span class=keyword>while</span> (<span class=literal>true</span>) {</span><br><span class=line> <span class=comment>// 初始为 true,保证没有排队消息时也至少请求一次 LLM。</span></span><br><span class=line> <span class=comment>// 后续它表示上一条 assistant message 是否留下了待反馈的工具结果。</span></span><br><span class=line> <span class=keyword>let</span> hasMoreToolCalls = <span class=literal>true</span>;</span><br><span class=line></span><br><span class=line> <span class=comment>// 内层:只要还有工具结果需要交给模型,或有 steering 消息,就继续</span></span><br><span class=line> <span class=keyword>while</span> (hasMoreToolCalls || pendingMessages.<span class=property>length</span> > <span class=number>0</span>) {</span><br><span class=line> <span class=comment>// 第一轮会跳过。第二轮起,在每次 LLM 请求前给上层一次机会,</span></span><br><span class=line> <span class=comment>// 用上一轮完整结果更新上下文、模型或 thinking level。</span></span><br><span class=line> <span class=keyword>if</span> (lastCompletedTurn) {</span><br><span class=line> <span class=keyword>const</span> nextTurnSnapshot =</span><br><span class=line> <span class=keyword>await</span> config.<span class=property>prepareNextTurn</span>?.(lastCompletedTurn);</span><br><span class=line> <span class=keyword>if</span> (nextTurnSnapshot) {</span><br><span class=line> <span class=comment>// hook 可以只返回需要替换的字段,其余部分继续沿用。</span></span><br><span class=line> currentContext =</span><br><span class=line> nextTurnSnapshot.<span class=property>context</span> ?? currentContext;</span><br><span class=line> config = {</span><br><span class=line> ...config,</span><br><span class=line> <span class=attr>model</span>: nextTurnSnapshot.<span class=property>model</span> ?? config.<span class=property>model</span>,</span><br><span class=line> <span class=attr>reasoning</span>:</span><br><span class=line> nextTurnSnapshot.<span class=property>thinkingLevel</span> === <span class=literal>undefined</span></span><br><span class=line> ? config.<span class=property>reasoning</span></span><br><span class=line> : nextTurnSnapshot.<span class=property>thinkingLevel</span> === <span class=string>"off"</span></span><br><span class=line> ? <span class=literal>undefined</span></span><br><span class=line> : nextTurnSnapshot.<span class=property>thinkingLevel</span>,</span><br><span class=line> };</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// prepareNextTurn 可能在做耗时的上下文压缩。压缩期间到达的</span></span><br><span class=line> <span class=comment>// steering 也要进入下一轮;已有 pending 时不再拉取,避免</span></span><br><span class=line> <span class=comment>// one-at-a-time 模式在一个 turn 中取出两条消息。</span></span><br><span class=line> <span class=keyword>if</span> (pendingMessages.<span class=property>length</span> === <span class=number>0</span>) {</span><br><span class=line> pendingMessages =</span><br><span class=line> (<span class=keyword>await</span> config.<span class=property>getSteeringMessages</span>?.()) || [];</span><br><span class=line> }</span><br><span class=line> <span class=comment>// turn_start 放在准备结束后,表示下一轮现在才正式开始。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"turn_start"</span> });</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 在下一次 assistant 响应前注入 steering 消息</span></span><br><span class=line> <span class=keyword>if</span> (pendingMessages.<span class=property>length</span> > <span class=number>0</span>) {</span><br><span class=line> <span class=keyword>for</span> (<span class=keyword>const</span> message <span class=keyword>of</span> pendingMessages) {</span><br><span class=line> <span class=comment>// 先发标准消息事件供 Session/UI 消费,再同步写入两个数组。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"message_start"</span>, message });</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"message_end"</span>, message });</span><br><span class=line> currentContext.<span class=property>messages</span>.<span class="title function_">push</span>(message);</span><br><span class=line> newMessages.<span class="title function_">push</span>(message);</span><br><span class=line> }</span><br><span class=line> <span class=comment>// 已转移到上下文,必须清空,防止下个 turn 重复注入。</span></span><br><span class=line> pendingMessages = [];</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 一次真正的 LLM 请求与流式响应消费。函数返回时,</span></span><br><span class=line> <span class=comment>// currentContext 最后一项已经从 partial message 变成最终消息。</span></span><br><span class=line> <span class=keyword>const</span> message = <span class=keyword>await</span> <span class="title function_">streamAssistantResponse</span>(</span><br><span class=line> currentContext,</span><br><span class=line> config,</span><br><span class=line> signal,</span><br><span class=line> emit,</span><br><span class=line> streamFunction,</span><br><span class=line> );</span><br><span class=line> <span class=comment>// streamAssistantResponse 已维护完整上下文;这里只记录本次增量。</span></span><br><span class=line> newMessages.<span class="title function_">push</span>(message);</span><br><span class=line></span><br><span class=line> <span class=comment>// provider 将请求失败与取消编码进最终 AssistantMessage。</span></span><br><span class=line> <span class=comment>// 这两种情况直接正常发出结束事件,不再执行工具或读取队列。</span></span><br><span class=line> <span class=keyword>if</span> (</span><br><span class=line> message.<span class=property>stopReason</span> === <span class=string>"error"</span> ||</span><br><span class=line> message.<span class=property>stopReason</span> === <span class=string>"aborted"</span></span><br><span class=line> ) {</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"turn_end"</span>, message, <span class=attr>toolResults</span>: [] });</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"agent_end"</span>, <span class=attr>messages</span>: newMessages });</span><br><span class=line> <span class=keyword>return</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>const</span> toolCalls = message.<span class=property>content</span>.<span class="title function_">filter</span>(</span><br><span class=line> <span class=function>(<span class=params>c</span>) =></span> c.<span class=property>type</span> === <span class=string>"toolCall"</span>,</span><br><span class=line> );</span><br><span class=line> <span class=comment>// turn_end 始终需要统一的数组结构,所以无工具时也创建空数组。</span></span><br><span class=line> <span class=keyword>const</span> <span class=attr>toolResults</span>: <span class="title class_">ToolResultMessage</span>[] = [];</span><br><span class=line> <span class=comment>// 先假设工具链结束。只有确实执行过工具且批次未要求终止,</span></span><br><span class=line> <span class=comment>// 才会在下面把它恢复为 true。</span></span><br><span class=line> hasMoreToolCalls = <span class=literal>false</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (toolCalls.<span class=property>length</span> > <span class=number>0</span>) {</span><br><span class=line> <span class=comment>// length 表示 assistant 输出被 token 上限截断。</span></span><br><span class=line> <span class=comment>// 此时所有 toolCall 参数都不可信,即使其中某个 JSON 可以解析。</span></span><br><span class=line> <span class=keyword>const</span> executedToolBatch =</span><br><span class=line> message.<span class=property>stopReason</span> === <span class=string>"length"</span></span><br><span class=line> ? <span class=keyword>await</span> <span class="title function_">failToolCallsFromTruncatedMessage</span>(</span><br><span class=line> toolCalls,</span><br><span class=line> emit,</span><br><span class=line> )</span><br><span class=line> : <span class=keyword>await</span> <span class="title function_">executeToolCalls</span>(</span><br><span class=line> currentContext,</span><br><span class=line> message,</span><br><span class=line> config,</span><br><span class=line> signal,</span><br><span class=line> emit,</span><br><span class=line> );</span><br><span class=line></span><br><span class=line> toolResults.<span class="title function_">push</span>(...executedToolBatch.<span class=property>messages</span>);</span><br><span class=line> <span class=comment>// 普通工具结果必须再交给 LLM,总结结果或决定下一步。</span></span><br><span class=line> <span class=comment>// 只有工具批次明确 terminate 时,才不自动继续这一工具链。</span></span><br><span class=line> hasMoreToolCalls = !executedToolBatch.<span class=property>terminate</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>for</span> (<span class=keyword>const</span> result <span class=keyword>of</span> toolResults) {</span><br><span class=line> <span class=comment>// 完整上下文决定下次发给 LLM 什么;newMessages 只负责</span></span><br><span class=line> <span class=comment>// 本次 Agent Run 的返回值和 agent_end 事件。</span></span><br><span class=line> currentContext.<span class=property>messages</span>.<span class="title function_">push</span>(result);</span><br><span class=line> newMessages.<span class="title function_">push</span>(result);</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 一个 turn 包含一次 assistant 响应及其引发的整批工具执行,</span></span><br><span class=line> <span class=comment>// 所以 turn_end 必须等所有 toolResults 准备好后再发送。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"turn_end"</span>, message, toolResults });</span><br><span class=line></span><br><span class=line> <span class=comment>// 下一轮的 hook 需要同时看到响应、工具结果、完整上下文和运行增量。</span></span><br><span class=line> lastCompletedTurn = {</span><br><span class=line> message,</span><br><span class=line> toolResults,</span><br><span class=line> <span class=attr>context</span>: currentContext,</span><br><span class=line> newMessages,</span><br><span class=line> };</span><br><span class=line></span><br><span class=line> <span class=comment>// “优雅停止”发生在当前 turn 完整结束后,优先于队列轮询。</span></span><br><span class=line> <span class=comment>// 因而不会中断已开始的工具,但会结束本次 Agent Run。</span></span><br><span class=line> <span class=keyword>if</span> (<span class=keyword>await</span> config.<span class=property>shouldStopAfterTurn</span>?.(lastCompletedTurn)) {</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"agent_end"</span>, <span class=attr>messages</span>: newMessages });</span><br><span class=line> <span class=keyword>return</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 模型响应与工具执行期间新到达的 steering 在这里读取。</span></span><br><span class=line> <span class=comment>// 非空时,下一次 while 条件成立并开始一个新 turn。</span></span><br><span class=line> pendingMessages =</span><br><span class=line> (<span class=keyword>await</span> config.<span class=property>getSteeringMessages</span>?.()) || [];</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 内层结束,Agent 本来准备停止;再检查延后执行的 follow-up</span></span><br><span class=line> <span class=keyword>const</span> followUpMessages =</span><br><span class=line> (<span class=keyword>await</span> config.<span class=property>getFollowUpMessages</span>?.()) || [];</span><br><span class=line> <span class=keyword>if</span> (followUpMessages.<span class=property>length</span> > <span class=number>0</span>) {</span><br><span class=line> <span class=comment>// 复用 pendingMessages 的标准注入路径,使 follow-up 同样产生</span></span><br><span class=line> <span class=comment>// message_start/message_end,并进入上下文与本次运行增量。</span></span><br><span class=line> pendingMessages = followUpMessages;</span><br><span class=line> <span class=comment>// 回到外层会重新令 hasMoreToolCalls=true,所以 follow-up</span></span><br><span class=line> <span class=comment>// 至少触发一次 LLM 请求,而不只是被追加到历史。</span></span><br><span class=line> <span class=keyword>continue</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 工具链、steering 和 follow-up 都耗尽,才退出整个 Agent Run。</span></span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// agent_end 是本次 run 的最后一个协议事件,只携带本次新增消息。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"agent_end"</span>, <span class=attr>messages</span>: newMessages });</span><br><span class=line>}</span><br></pre></table></figure><h3 id=1-内层循环:完成当前任务链><a title="1. 内层循环:完成当前任务链" class=headerlink href=#1-内层循环:完成当前任务链></a>1. 内层循环:完成当前任务链</h3><p>内层条件是:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>while</span> (hasMoreToolCalls || pendingMessages.<span class=property>length</span> > <span class=number>0</span>)</span><br></pre></table></figure><p>初始时 <code>hasMoreToolCalls = true</code>,所以至少请求模型一次。<p>假设用户说“读取 package.json 并告诉我项目名称”,一次典型轨迹是:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br></pre><td class=code><pre><span class=line>第一次 LLM 请求</span><br><span class=line> → assistant 返回 read 工具调用</span><br><span class=line> → 执行 read</span><br><span class=line> → 把 toolResult 追加到 currentContext.messages</span><br><span class=line> → hasMoreToolCalls = true</span><br><span class=line></span><br><span class=line>第二次 LLM 请求</span><br><span class=line> → 模型看到 read 的结果</span><br><span class=line> → assistant 返回最终文本,不再调用工具</span><br><span class=line> → hasMoreToolCalls = false</span><br><span class=line> → 如果也没有 steering,退出内层循环</span><br></pre></table></figure><p>所以工具调用后的“下一轮”不是递归,而是同一个内层 <code>while</code> 的下一次迭代。<p><code>steering</code> 也在内层处理。它的含义是:当前 Agent 还在工作时,用户发来一条纠偏消息。这条消息不会强行中断正在消费的模型响应或正在执行的工具,而是在当前 turn 完整结束后、下一次 LLM 请求前注入上下文。<h3 id=2-外层循环:处理-Agent-停止后的-follow-up><a title="2. 外层循环:处理 Agent 停止后的 follow-up" class=headerlink href=#2-外层循环:处理-Agent-停止后的-follow-up></a>2. 外层循环:处理 Agent 停止后的 follow-up</h3><p>当工具链结束且没有 steering 时,内层循环退出。此时 Agent 原本应该结束,但外层会再查询一次 <code>getFollowUpMessages()</code>。<p><code>followUp</code> 与 <code>steering</code> 的差别是:<ul><li><code>steering</code> 参与当前任务链,尽快在下一 turn 纠偏;<li><code>followUp</code> 等当前任务自然完成后再开始,更像排队的下一项任务。</ul><p>如果有 follow-up,代码把它放进 <code>pendingMessages</code>,通过 <code>continue</code> 回到外层顶部,再进入内层循环。没有 follow-up 才真正 <code>break</code> 并发出 <code>agent_end</code>。<h3 id=3-prepareNextTurn-为什么放在请求前><a title="3. prepareNextTurn 为什么放在请求前" class=headerlink href=#3-prepareNextTurn-为什么放在请求前></a>3. prepareNextTurn 为什么放在请求前</h3><p>从第二个 turn 开始,代码会调用 <code>prepareNextTurn(lastCompletedTurn)</code>。它允许上层在下一次请求前替换:<ul><li>context,例如完成上下文压缩;<li>model,例如运行途中切换模型;<li>thinking level。</ul><p>这就是为什么它必须处在 <code>streamAssistantResponse()</code> 之前,而且只在已有 <code>lastCompletedTurn</code> 时执行。<h3 id=4-length-时为什么不执行工具><a title="4. length 时为什么不执行工具" class=headerlink href=#4-length-时为什么不执行工具></a>4. length 时为什么不执行工具</h3><p>如果模型因为输出 token 上限而以 <code>stopReason === "length"</code> 结束,工具参数也可能被截断。即使残缺 JSON 被宽松解析器碰巧修复成功,也不能保证语义完整。因此代码不会执行任何一个工具,而是给每个调用生成错误结果,让模型在下一轮重新发起完整调用。<p>这是一个很容易被简化版 Agent Loop 忽略的安全细节。<h2 id=十、一次-LLM-流式响应是怎样被消费的><a title="十、一次 LLM 流式响应是怎样被消费的" class=headerlink href=#十、一次-LLM-流式响应是怎样被消费的></a>十、一次 LLM 流式响应是怎样被消费的</h2><p><code>streamAssistantResponse()</code> 是 Agent Core 与模型流的直接边界:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br><span class=line>50</span><br><span class=line>51</span><br><span class=line>52</span><br><span class=line>53</span><br><span class=line>54</span><br><span class=line>55</span><br><span class=line>56</span><br><span class=line>57</span><br><span class=line>58</span><br><span class=line>59</span><br><span class=line>60</span><br><span class=line>61</span><br><span class=line>62</span><br><span class=line>63</span><br><span class=line>64</span><br><span class=line>65</span><br><span class=line>66</span><br><span class=line>67</span><br><span class=line>68</span><br><span class=line>69</span><br><span class=line>70</span><br><span class=line>71</span><br><span class=line>72</span><br><span class=line>73</span><br><span class=line>74</span><br><span class=line>75</span><br><span class=line>76</span><br><span class=line>77</span><br><span class=line>78</span><br><span class=line>79</span><br><span class=line>80</span><br><span class=line>81</span><br><span class=line>82</span><br><span class=line>83</span><br><span class=line>84</span><br><span class=line>85</span><br><span class=line>86</span><br><span class=line>87</span><br><span class=line>88</span><br><span class=line>89</span><br><span class=line>90</span><br><span class=line>91</span><br><span class=line>92</span><br><span class=line>93</span><br><span class=line>94</span><br><span class=line>95</span><br><span class=line>96</span><br><span class=line>97</span><br><span class=line>98</span><br><span class=line>99</span><br><span class=line>100</span><br><span class=line>101</span><br><span class=line>102</span><br><span class=line>103</span><br><span class=line>104</span><br><span class=line>105</span><br><span class=line>106</span><br><span class=line>107</span><br><span class=line>108</span><br><span class=line>109</span><br><span class=line>110</span><br><span class=line>111</span><br><span class=line>112</span><br><span class=line>113</span><br><span class=line>114</span><br><span class=line>115</span><br><span class=line>116</span><br><span class=line>117</span><br><span class=line>118</span><br><span class=line>119</span><br><span class=line>120</span><br><span class=line>121</span><br><span class=line>122</span><br><span class=line>123</span><br><span class=line>124</span><br><span class=line>125</span><br><span class=line>126</span><br><span class=line>127</span><br><span class=line>128</span><br><span class=line>129</span><br><span class=line>130</span><br><span class=line>131</span><br><span class=line>132</span><br><span class=line>133</span><br><span class=line>134</span><br><span class=line>135</span><br><span class=line>136</span><br><span class=line>137</span><br></pre><td class=code><pre><span class=line><span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">streamAssistantResponse</span>(<span class=params></span></span><br><span class=line><span class=params> context: AgentContext,</span></span><br><span class=line><span class=params> config: AgentLoopConfig,</span></span><br><span class=line><span class=params> signal: AbortSignal | <span class=literal>undefined</span>,</span></span><br><span class=line><span class=params> emit: AgentEventSink,</span></span><br><span class=line><span class=params> streamFunction: StreamFn,</span></span><br><span class=line><span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">AssistantMessage</span>> {</span><br><span class=line> <span class=comment>// context.messages 是循环内的权威历史。transformContext 不直接写回它,</span></span><br><span class=line> <span class=comment>// 而是生成“仅用于本次请求”的视图,可用于压缩、裁剪或扩展上下文。</span></span><br><span class=line> <span class=comment>// AgentMessage[] → AgentMessage[],此时仍允许包含产品层 custom message。</span></span><br><span class=line> <span class=keyword>let</span> messages = context.<span class=property>messages</span>;</span><br><span class=line> <span class=keyword>if</span> (config.<span class=property>transformContext</span>) {</span><br><span class=line> messages = <span class=keyword>await</span> config.<span class="title function_">transformContext</span>(messages, signal);</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// AgentMessage[] → pi-ai Message[]。UI 通知等模型不认识的消息在这里过滤,</span></span><br><span class=line> <span class=comment>// custom message 也必须在这里变换成 user/assistant/toolResult 之一。</span></span><br><span class=line> <span class=keyword>const</span> llmMessages = <span class=keyword>await</span> config.<span class="title function_">convertToLlm</span>(messages);</span><br><span class=line></span><br><span class=line> <span class=comment>// 把 system prompt、已转换消息和工具 schema 组成一次完整模型输入。</span></span><br><span class=line> <span class=comment>// 注意这里传的是工具“定义”,真正的 execute 函数仍只保留在 Agent Context。</span></span><br><span class=line> <span class=keyword>const</span> <span class=attr>llmContext</span>: <span class="title class_">Context</span> = {</span><br><span class=line> <span class=attr>systemPrompt</span>: context.<span class=property>systemPrompt</span>,</span><br><span class=line> <span class=attr>messages</span>: llmMessages,</span><br><span class=line> <span class=attr>tools</span>: context.<span class=property>tools</span>,</span><br><span class=line> };</span><br><span class=line></span><br><span class=line> <span class=comment>// 每次请求都重新取 key,兼容会过期和刷新的 OAuth token。</span></span><br><span class=line> <span class=comment>// getApiKey 的动态结果优先;没有结果时才回退到 config.apiKey。</span></span><br><span class=line> <span class=keyword>const</span> resolvedApiKey =</span><br><span class=line> (config.<span class=property>getApiKey</span></span><br><span class=line> ? <span class=keyword>await</span> config.<span class="title function_">getApiKey</span>(config.<span class=property>model</span>.<span class=property>provider</span>)</span><br><span class=line> : <span class=literal>undefined</span>) || config.<span class=property>apiKey</span>;</span><br><span class=line></span><br><span class=line> <span class=comment>// streamFunction 返回可异步迭代的 AssistantMessageEventStream,</span></span><br><span class=line> <span class=comment>// 调用到这里并不代表完整回答已经到达。</span></span><br><span class=line> <span class=keyword>const</span> response = <span class=keyword>await</span> <span class="title function_">streamFunction</span>(config.<span class=property>model</span>, llmContext, {</span><br><span class=line> <span class=comment>// config 继承 SimpleStreamOptions,因此请求 hook、thinking、transport 等</span></span><br><span class=line> <span class=comment>// 选项先整体透传,再用下面两个字段覆盖成当前调用的最终值。</span></span><br><span class=line> ...config,</span><br><span class=line> <span class=attr>apiKey</span>: resolvedApiKey,</span><br><span class=line> signal,</span><br><span class=line> });</span><br><span class=line></span><br><span class=line> <span class=comment>// partialMessage 始终指向“截至当前事件已经拼装出的完整消息”,</span></span><br><span class=line> <span class=comment>// 而不是单独的 token/delta。</span></span><br><span class=line> <span class=keyword>let</span> <span class=attr>partialMessage</span>: <span class="title class_">AssistantMessage</span> | <span class=literal>null</span> = <span class=literal>null</span>;</span><br><span class=line> <span class=comment>// 标记 context.messages 中是否已经为这个回答占了最后一个位置。</span></span><br><span class=line> <span class=comment>// 它用于兼容没有 start 事件、直接结束的 provider 流。</span></span><br><span class=line> <span class=keyword>let</span> addedPartial = <span class=literal>false</span>;</span><br><span class=line></span><br><span class=line> <span class=comment>// Provider 事件按产生顺序异步到达;for await 自然形成背压,</span></span><br><span class=line> <span class=comment>// 当前事件处理和 emit 没结束前不会消费下一事件。</span></span><br><span class=line> <span class=keyword>for</span> <span class=keyword>await</span> (<span class=keyword>const</span> event <span class=keyword>of</span> response) {</span><br><span class=line> <span class=keyword>switch</span> (event.<span class=property>type</span>) {</span><br><span class=line> <span class=keyword>case</span> <span class=string>"start"</span>:</span><br><span class=line> <span class=comment>// start.partial 通常只有 assistant 消息的骨架和空 content。</span></span><br><span class=line> partialMessage = event.<span class=property>partial</span>;</span><br><span class=line> <span class=comment>// 先把骨架放到历史末尾,为后面的 delta 保留固定槽位。</span></span><br><span class=line> context.<span class=property>messages</span>.<span class="title function_">push</span>(partialMessage);</span><br><span class=line> addedPartial = <span class=literal>true</span>;</span><br><span class=line> <span class=comment>// 展开成新对象,避免监听者持有随后会变化的同一对象引用。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({</span><br><span class=line> <span class=attr>type</span>: <span class=string>"message_start"</span>,</span><br><span class=line> <span class=attr>message</span>: { ...partialMessage },</span><br><span class=line> });</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"text_start"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"text_delta"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"text_end"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"thinking_start"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"thinking_delta"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"thinking_end"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"toolcall_start"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"toolcall_delta"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"toolcall_end"</span>:</span><br><span class=line> <span class=comment>// 所有内容事件走同一条归并路径。event.partial 已由 pi-ai</span></span><br><span class=line> <span class=comment>// 累积好当前文本、thinking 或 toolCall,不需要 Core 自己拼 token。</span></span><br><span class=line> <span class=keyword>if</span> (partialMessage) {</span><br><span class=line> partialMessage = event.<span class=property>partial</span>;</span><br><span class=line> <span class=comment>// 替换最后一项而非 push:一个回答无论包含多少 delta,</span></span><br><span class=line> <span class=comment>// 在上下文中始终只占一条 assistant message。</span></span><br><span class=line> context.<span class=property>messages</span>[context.<span class=property>messages</span>.<span class=property>length</span> - <span class=number>1</span>] =</span><br><span class=line> partialMessage;</span><br><span class=line> <span class=comment>// 原始 provider 事件一并向上传递,TUI 可区分文本、思考和工具参数。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({</span><br><span class=line> <span class=attr>type</span>: <span class=string>"message_update"</span>,</span><br><span class=line> <span class=attr>assistantMessageEvent</span>: event,</span><br><span class=line> <span class=attr>message</span>: { ...partialMessage },</span><br><span class=line> });</span><br><span class=line> }</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"done"</span>:</span><br><span class=line> <span class=keyword>case</span> <span class=string>"error"</span>: {</span><br><span class=line> <span class=comment>// result() 给出标准化后的最终 AssistantMessage,包含 usage、</span></span><br><span class=line> <span class=comment>// stopReason、errorMessage 以及最终 content。</span></span><br><span class=line> <span class=keyword>const</span> finalMessage = <span class=keyword>await</span> response.<span class="title function_">result</span>();</span><br><span class=line> <span class=keyword>if</span> (addedPartial) {</span><br><span class=line> <span class=comment>// 正常流:用最终对象替换之前反复更新的临时槽位。</span></span><br><span class=line> context.<span class=property>messages</span>[context.<span class=property>messages</span>.<span class=property>length</span> - <span class=number>1</span>] =</span><br><span class=line> finalMessage;</span><br><span class=line> } <span class=keyword>else</span> {</span><br><span class=line> <span class=comment>// 防御非常规 provider:没有 start 事件时在结束处补入历史。</span></span><br><span class=line> context.<span class=property>messages</span>.<span class="title function_">push</span>(finalMessage);</span><br><span class=line> }</span><br><span class=line> <span class=keyword>if</span> (!addedPartial) {</span><br><span class=line> <span class=comment>// 保持 Agent 事件协议完整:即使 provider 没 start,</span></span><br><span class=line> <span class=comment>// 上层仍会按顺序收到 message_start → message_end。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({</span><br><span class=line> <span class=attr>type</span>: <span class=string>"message_start"</span>,</span><br><span class=line> <span class=attr>message</span>: { ...finalMessage },</span><br><span class=line> });</span><br><span class=line> }</span><br><span class=line> <span class=comment>// message_end 才表示这条消息可以进入 Agent.state.messages。</span></span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"message_end"</span>, <span class=attr>message</span>: finalMessage });</span><br><span class=line> <span class=keyword>return</span> finalMessage;</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 有些实现通过结束 AsyncIterable 表示完成,不额外发 done。</span></span><br><span class=line> <span class=comment>// 这个兜底分支与上面的 done/error 分支保持完全相同的落库和事件语义。</span></span><br><span class=line> <span class=keyword>const</span> finalMessage = <span class=keyword>await</span> response.<span class="title function_">result</span>();</span><br><span class=line> <span class=keyword>if</span> (addedPartial) {</span><br><span class=line> context.<span class=property>messages</span>[context.<span class=property>messages</span>.<span class=property>length</span> - <span class=number>1</span>] = finalMessage;</span><br><span class=line> } <span class=keyword>else</span> {</span><br><span class=line> context.<span class=property>messages</span>.<span class="title function_">push</span>(finalMessage);</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({</span><br><span class=line> <span class=attr>type</span>: <span class=string>"message_start"</span>,</span><br><span class=line> <span class=attr>message</span>: { ...finalMessage },</span><br><span class=line> });</span><br><span class=line> }</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">emit</span>({ <span class=attr>type</span>: <span class=string>"message_end"</span>, <span class=attr>message</span>: finalMessage });</span><br><span class=line> <span class=keyword>return</span> finalMessage;</span><br><span class=line>}</span><br></pre></table></figure><p>这里可以看到三层事件:<ol><li>Provider 流事件:<code>text_delta</code>、<code>thinking_delta</code>、<code>toolcall_delta</code> 等;<li>Agent 事件:<code>message_start</code>、<code>message_update</code>、<code>message_end</code>;<li>更外层 UI 订阅 Agent 事件,刷新终端内容和会话状态。</ol><p>收到 <code>start</code> 时,代码先把 partial message 放进 <code>context.messages</code>。后续每个 delta 都不是继续 push 新消息,而是替换数组最后一项。否则每个 token 都会变成一条历史消息。<p>收到 <code>done</code> 或 <code>error</code> 后,再通过 <code>response.result()</code> 取得最终对象,用它替换 partial message。末尾的兜底逻辑用于处理流正常耗尽但没有显式 <code>done/error</code> 事件的情况。<h2 id=十一、工具调用:选择、校验、执行与回填><a class=headerlink href=#十一、工具调用:选择、校验、执行与回填 title=十一、工具调用:选择、校验、执行与回填></a>十一、工具调用:选择、校验、执行与回填</h2><p>模型响应完成后,主循环从 <code>message.content</code> 中筛出工具调用。实际执行入口如下:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br></pre><td class=code><pre><span class=line><span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">executeToolCalls</span>(<span class=params></span></span><br><span class=line><span class=params> currentContext: AgentContext,</span></span><br><span class=line><span class=params> assistantMessage: AssistantMessage,</span></span><br><span class=line><span class=params> config: AgentLoopConfig,</span></span><br><span class=line><span class=params> signal: AbortSignal | <span class=literal>undefined</span>,</span></span><br><span class=line><span class=params> emit: AgentEventSink,</span></span><br><span class=line><span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">ExecutedToolCallBatch</span>> {</span><br><span class=line> <span class=comment>// 再从最终 assistant message 中提取一次,确保执行的是已经完成流式拼装的调用。</span></span><br><span class=line> <span class=keyword>const</span> toolCalls = assistantMessage.<span class=property>content</span>.<span class="title function_">filter</span>(</span><br><span class=line> <span class=function>(<span class=params>c</span>) =></span> c.<span class=property>type</span> === <span class=string>"toolCall"</span>,</span><br><span class=line> );</span><br><span class=line> <span class=comment>// 工具自身可声明 sequential。例如修改同一状态的工具不能安全并发。</span></span><br><span class=line> <span class=comment>// 只要批次中有一个这样的工具,整批都按串行处理,避免部分交错。</span></span><br><span class=line> <span class=keyword>const</span> hasSequentialToolCall = toolCalls.<span class="title function_">some</span>(</span><br><span class=line> <span class=function>(<span class=params>tc</span>) =></span></span><br><span class=line> currentContext.<span class=property>tools</span>?.<span class="title function_">find</span>(<span class=function>(<span class=params>t</span>) =></span> t.<span class=property>name</span> === tc.<span class=property>name</span>)</span><br><span class=line> ?.<span class=property>executionMode</span> === <span class=string>"sequential"</span>,</span><br><span class=line> );</span><br><span class=line></span><br><span class=line> <span class=comment>// 全局策略和工具局部声明任一要求串行,就走串行实现。</span></span><br><span class=line> <span class=keyword>if</span> (config.<span class=property>toolExecution</span> === <span class=string>"sequential"</span> || hasSequentialToolCall) {</span><br><span class=line> <span class=keyword>return</span> <span class="title function_">executeToolCallsSequential</span>(</span><br><span class=line> currentContext,</span><br><span class=line> assistantMessage,</span><br><span class=line> toolCalls,</span><br><span class=line> config,</span><br><span class=line> signal,</span><br><span class=line> emit,</span><br><span class=line> );</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 默认并行路径仍会先顺序执行参数校验和 beforeToolCall,</span></span><br><span class=line> <span class=comment>// 只有通过预检的 tool.execute 才会并发。</span></span><br><span class=line> <span class=keyword>return</span> <span class="title function_">executeToolCallsParallel</span>(</span><br><span class=line> currentContext,</span><br><span class=line> assistantMessage,</span><br><span class=line> toolCalls,</span><br><span class=line> config,</span><br><span class=line> signal,</span><br><span class=line> emit,</span><br><span class=line> );</span><br><span class=line>}</span><br></pre></table></figure><p>默认策略是并行,但只要本批次任意工具声明 <code>executionMode === "sequential"</code>,整批就改为串行。这样有副作用或有顺序依赖的工具可以主动要求保守执行。<p>每个工具真正运行前还要经过 <code>prepareToolCall()</code>:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br><span class=line>50</span><br><span class=line>51</span><br><span class=line>52</span><br><span class=line>53</span><br><span class=line>54</span><br><span class=line>55</span><br><span class=line>56</span><br><span class=line>57</span><br><span class=line>58</span><br><span class=line>59</span><br><span class=line>60</span><br><span class=line>61</span><br><span class=line>62</span><br><span class=line>63</span><br><span class=line>64</span><br><span class=line>65</span><br><span class=line>66</span><br><span class=line>67</span><br><span class=line>68</span><br><span class=line>69</span><br><span class=line>70</span><br><span class=line>71</span><br><span class=line>72</span><br><span class=line>73</span><br><span class=line>74</span><br><span class=line>75</span><br><span class=line>76</span><br><span class=line>77</span><br><span class=line>78</span><br><span class=line>79</span><br><span class=line>80</span><br><span class=line>81</span><br><span class=line>82</span><br></pre><td class=code><pre><span class=line><span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">prepareToolCall</span>(<span class=params></span></span><br><span class=line><span class=params> currentContext: AgentContext,</span></span><br><span class=line><span class=params> assistantMessage: AssistantMessage,</span></span><br><span class=line><span class=params> toolCall: AgentToolCall,</span></span><br><span class=line><span class=params> config: AgentLoopConfig,</span></span><br><span class=line><span class=params> signal: AbortSignal | <span class=literal>undefined</span>,</span></span><br><span class=line><span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">PreparedToolCall</span> | <span class="title class_">ImmediateToolCallOutcome</span>> {</span><br><span class=line> <span class=comment>// 模型只能按名称调用当前上下文公开的工具,不能凭空调用任意函数。</span></span><br><span class=line> <span class=keyword>const</span> tool = currentContext.<span class=property>tools</span>?.<span class="title function_">find</span>(</span><br><span class=line> <span class=function>(<span class=params>t</span>) =></span> t.<span class=property>name</span> === toolCall.<span class=property>name</span>,</span><br><span class=line> );</span><br><span class=line> <span class=keyword>if</span> (!tool) {</span><br><span class=line> <span class=comment>// “工具不存在”也规范化成即时结果,让 LLM 有机会改正名称。</span></span><br><span class=line> <span class=keyword>return</span> {</span><br><span class=line> <span class=attr>kind</span>: <span class=string>"immediate"</span>,</span><br><span class=line> <span class=attr>result</span>: <span class="title function_">createErrorToolResult</span>(</span><br><span class=line> <span class=string>`Tool <span class=subst>${toolCall.name}</span> not found`</span>,</span><br><span class=line> ),</span><br><span class=line> <span class=attr>isError</span>: <span class=literal>true</span>,</span><br><span class=line> };</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>try</span> {</span><br><span class=line> <span class=comment>// prepareArguments 可做兼容性归一化;随后必须按工具 schema 校验。</span></span><br><span class=line> <span class=comment>// validatedArgs 才是最终允许交给 execute 的参数。</span></span><br><span class=line> <span class=keyword>const</span> preparedToolCall = <span class="title function_">prepareToolCallArguments</span>(tool, toolCall);</span><br><span class=line> <span class=keyword>const</span> validatedArgs = <span class="title function_">validateToolArguments</span>(tool, preparedToolCall);</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (config.<span class=property>beforeToolCall</span>) {</span><br><span class=line> <span class=comment>// hook 能看到 assistant 原消息、原 toolCall、已校验参数和上下文,</span></span><br><span class=line> <span class=comment>// 可用于权限确认、策略限制、日志或参数审计。</span></span><br><span class=line> <span class=keyword>const</span> beforeResult = <span class=keyword>await</span> config.<span class="title function_">beforeToolCall</span>(</span><br><span class=line> {</span><br><span class=line> assistantMessage,</span><br><span class=line> toolCall,</span><br><span class=line> <span class=attr>args</span>: validatedArgs,</span><br><span class=line> <span class=attr>context</span>: currentContext,</span><br><span class=line> },</span><br><span class=line> signal,</span><br><span class=line> );</span><br><span class=line></span><br><span class=line> <span class=comment>// hook 等待期间用户可能取消,因此返回后立即再次检查 signal。</span></span><br><span class=line> <span class=keyword>if</span> (signal?.<span class=property>aborted</span>) {</span><br><span class=line> <span class=keyword>return</span> {</span><br><span class=line> <span class=attr>kind</span>: <span class=string>"immediate"</span>,</span><br><span class=line> <span class=attr>result</span>: <span class="title function_">createErrorToolResult</span>(<span class=string>"Operation aborted"</span>),</span><br><span class=line> <span class=attr>isError</span>: <span class=literal>true</span>,</span><br><span class=line> };</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>if</span> (beforeResult?.<span class=property>block</span>) {</span><br><span class=line> <span class=comment>// block 不抛异常,而是产生模型可读的错误 toolResult。</span></span><br><span class=line> <span class=keyword>const</span> result = <span class="title function_">createErrorToolResult</span>(</span><br><span class=line> beforeResult.<span class=property>reason</span> || <span class=string>"Tool execution was blocked"</span>,</span><br><span class=line> );</span><br><span class=line> <span class=keyword>if</span> (beforeResult.<span class=property>terminate</span> === <span class=literal>true</span>) {</span><br><span class=line> <span class=comment>// terminate 会参与整个批次的终止判断,而不是只标记 UI。</span></span><br><span class=line> result.<span class=property>terminate</span> = <span class=literal>true</span>;</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> { <span class=attr>kind</span>: <span class=string>"immediate"</span>, result, <span class=attr>isError</span>: <span class=literal>true</span> };</span><br><span class=line> }</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// prepared 是预检与执行之间的边界:只有这种结果会调用 tool.execute。</span></span><br><span class=line> <span class=keyword>return</span> {</span><br><span class=line> <span class=attr>kind</span>: <span class=string>"prepared"</span>,</span><br><span class=line> toolCall,</span><br><span class=line> tool,</span><br><span class=line> <span class=attr>args</span>: validatedArgs,</span><br><span class=line> };</span><br><span class=line> } <span class=keyword>catch</span> (error) {</span><br><span class=line> <span class=comment>// 参数预处理、schema 校验和 hook 抛错都降级为即时错误结果,</span></span><br><span class=line> <span class=comment>// 保证 Agent Loop 仍能把错误放回对话,而不是破坏事件序列。</span></span><br><span class=line> <span class=keyword>return</span> {</span><br><span class=line> <span class=attr>kind</span>: <span class=string>"immediate"</span>,</span><br><span class=line> <span class=attr>result</span>: <span class="title function_">createErrorToolResult</span>(</span><br><span class=line> error <span class=keyword>instanceof</span> <span class="title class_">Error</span> ? error.<span class=property>message</span> : <span class="title class_">String</span>(error),</span><br><span class=line> ),</span><br><span class=line> <span class=attr>isError</span>: <span class=literal>true</span>,</span><br><span class=line> };</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><p>它完成四项检查:<ol><li>工具是否存在;<li>是否需要预处理参数;<li>参数是否符合工具 schema;<li><code>beforeToolCall</code> hook 是否阻止执行。</ol><p>工具报错通常也不会炸掉整个 Agent Loop,而是转成 <code>isError: true</code> 的 <code>toolResult</code>。这样模型下一轮能看见错误,并决定修正参数、换一种方法或直接向用户解释失败。<p>工具执行结束后生成标准消息:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br></pre><td class=code><pre><span class=line><span class=keyword>function</span> <span class="title function_">createToolResultMessage</span>(<span class=params></span></span><br><span class=line><span class=params> finalized: FinalizedToolCallOutcome,</span></span><br><span class=line><span class=params></span>): <span class="title class_">ToolResultMessage</span> {</span><br><span class=line> <span class=keyword>return</span> {</span><br><span class=line> <span class=attr>role</span>: <span class=string>"toolResult"</span>,</span><br><span class=line> <span class=attr>toolCallId</span>: finalized.<span class=property>toolCall</span>.<span class=property>id</span>,</span><br><span class=line> <span class=attr>toolName</span>: finalized.<span class=property>toolCall</span>.<span class=property>name</span>,</span><br><span class=line> <span class=attr>content</span>: finalized.<span class=property>result</span>.<span class=property>content</span> ?? [],</span><br><span class=line> <span class=attr>details</span>: finalized.<span class=property>result</span>.<span class=property>details</span>,</span><br><span class=line> <span class=attr>usage</span>: finalized.<span class=property>result</span>.<span class=property>usage</span>,</span><br><span class=line> ...(finalized.<span class=property>result</span>.<span class=property>addedToolNames</span>?.<span class=property>length</span></span><br><span class=line> ? { <span class=attr>addedToolNames</span>: finalized.<span class=property>result</span>.<span class=property>addedToolNames</span> }</span><br><span class=line> : {}),</span><br><span class=line> <span class=attr>isError</span>: finalized.<span class=property>isError</span>,</span><br><span class=line> <span class=attr>timestamp</span>: <span class="title class_">Date</span>.<span class="title function_">now</span>(),</span><br><span class=line> };</span><br><span class=line>}</span><br></pre></table></figure><p>随后 <code>runLoop()</code> 把它加入 <code>currentContext.messages</code>。下一次调用 <code>streamAssistantResponse()</code> 时,<code>convertToLlm()</code> 会把这个 <code>toolResult</code> 转成模型协议需要的工具结果消息。至此完成闭环:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line>assistant toolCall</span><br><span class=line> → 参数准备与校验</span><br><span class=line> → beforeToolCall</span><br><span class=line> → tool.execute</span><br><span class=line> → afterToolCall</span><br><span class=line> → ToolResultMessage</span><br><span class=line> → 加入上下文</span><br><span class=line> → 下一次 LLM 请求</span><br></pre></table></figure><h2 id=十二、事件如何更新-Agent-状态><a title="十二、事件如何更新 Agent 状态" class=headerlink href=#十二、事件如何更新-Agent-状态></a>十二、事件如何更新 Agent 状态</h2><p>主循环只负责 <code>emit</code>,真正维护 <code>Agent.state</code> 的是 <code>agent.ts</code> 中的 <code>processEvents()</code>:<figure class="highlight typescript"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br><span class=line>9</span><br><span class=line>10</span><br><span class=line>11</span><br><span class=line>12</span><br><span class=line>13</span><br><span class=line>14</span><br><span class=line>15</span><br><span class=line>16</span><br><span class=line>17</span><br><span class=line>18</span><br><span class=line>19</span><br><span class=line>20</span><br><span class=line>21</span><br><span class=line>22</span><br><span class=line>23</span><br><span class=line>24</span><br><span class=line>25</span><br><span class=line>26</span><br><span class=line>27</span><br><span class=line>28</span><br><span class=line>29</span><br><span class=line>30</span><br><span class=line>31</span><br><span class=line>32</span><br><span class=line>33</span><br><span class=line>34</span><br><span class=line>35</span><br><span class=line>36</span><br><span class=line>37</span><br><span class=line>38</span><br><span class=line>39</span><br><span class=line>40</span><br><span class=line>41</span><br><span class=line>42</span><br><span class=line>43</span><br><span class=line>44</span><br><span class=line>45</span><br><span class=line>46</span><br><span class=line>47</span><br><span class=line>48</span><br><span class=line>49</span><br><span class=line>50</span><br><span class=line>51</span><br><span class=line>52</span><br><span class=line>53</span><br><span class=line>54</span><br><span class=line>55</span><br><span class=line>56</span><br><span class=line>57</span><br><span class=line>58</span><br><span class=line>59</span><br><span class=line>60</span><br><span class=line>61</span><br><span class=line>62</span><br><span class=line>63</span><br><span class=line>64</span><br><span class=line>65</span><br><span class=line>66</span><br></pre><td class=code><pre><span class=line><span class=keyword>private</span> <span class=keyword>async</span> <span class="title function_">processEvents</span>(<span class=attr>event</span>: <span class="title class_">AgentEvent</span>): <span class="title class_">Promise</span><<span class=built_in>void</span>> {</span><br><span class=line> <span class=comment>// 先归并 Agent 自己的可观察状态,再通知外部 listener。</span></span><br><span class=line> <span class=comment>// 因此外部收到事件时,读取 state 已经能看到与该事件一致的新值。</span></span><br><span class=line> <span class=keyword>switch</span> (event.<span class=property>type</span>) {</span><br><span class=line> <span class=keyword>case</span> <span class=string>"message_start"</span>:</span><br><span class=line> <span class=comment>// streamingMessage 是正在生成或刚开始发布的临时消息,</span></span><br><span class=line> <span class=comment>// 此时还不能放入正式历史,否则取消或 delta 会留下重复记录。</span></span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>streamingMessage</span> = event.<span class=property>message</span>;</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"message_update"</span>:</span><br><span class=line> <span class=comment>// 每个 delta 都用新的完整 partial snapshot 覆盖旧值。</span></span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>streamingMessage</span> = event.<span class=property>message</span>;</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"message_end"</span>:</span><br><span class=line> <span class=comment>// 只有结束事件才把消息追加到 Agent 的权威 transcript。</span></span><br><span class=line> <span class=comment>// agent-loop 内部的 currentContext 是独立数组,所以这里不是重复 push。</span></span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>streamingMessage</span> = <span class=literal>undefined</span>;</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>messages</span>.<span class="title function_">push</span>(event.<span class=property>message</span>);</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"tool_execution_start"</span>: {</span><br><span class=line> <span class=comment>// 创建新 Set 而不是原地 add,保证观察 state 引用变化的 UI 能刷新。</span></span><br><span class=line> <span class=keyword>const</span> pendingToolCalls = <span class=keyword>new</span> <span class="title class_">Set</span>(</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>pendingToolCalls</span>,</span><br><span class=line> );</span><br><span class=line> pendingToolCalls.<span class="title function_">add</span>(event.<span class=property>toolCallId</span>);</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>pendingToolCalls</span> = pendingToolCalls;</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"tool_execution_end"</span>: {</span><br><span class=line> <span class=comment>// 与 start 对称;并行模式下多个工具会按实际完成顺序依次移除。</span></span><br><span class=line> <span class=keyword>const</span> pendingToolCalls = <span class=keyword>new</span> <span class="title class_">Set</span>(</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>pendingToolCalls</span>,</span><br><span class=line> );</span><br><span class=line> pendingToolCalls.<span class="title function_">delete</span>(event.<span class=property>toolCallId</span>);</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>pendingToolCalls</span> = pendingToolCalls;</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"turn_end"</span>:</span><br><span class=line> <span class=comment>// 普通工具错误保存在 toolResult.isError;这里只记录 assistant 级错误。</span></span><br><span class=line> <span class=keyword>if</span> (event.<span class=property>message</span>.<span class=property>role</span> === <span class=string>"assistant"</span> && event.<span class=property>message</span>.<span class=property>errorMessage</span>) {</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>errorMessage</span> = event.<span class=property>message</span>.<span class=property>errorMessage</span>;</span><br><span class=line> }</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line></span><br><span class=line> <span class=keyword>case</span> <span class=string>"agent_end"</span>:</span><br><span class=line> <span class=comment>// agent_end 是事件流终点,但 activeRun 要等所有 listener 完成后</span></span><br><span class=line> <span class=comment>// 才会由 finishRun 清理,因此这里仅清空临时消息。</span></span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_state</span>.<span class=property>streamingMessage</span> = <span class=literal>undefined</span>;</span><br><span class=line> <span class=keyword>break</span>;</span><br><span class=line> }</span><br><span class=line></span><br><span class=line> <span class=comment>// 所有事件都必须发生在一次 activeRun 内,并共享这次运行的取消信号。</span></span><br><span class=line> <span class=keyword>const</span> signal = <span class="variable language_">this</span>.<span class=property>activeRun</span>?.<span class=property>abortController</span>.<span class=property>signal</span>;</span><br><span class=line> <span class=keyword>if</span> (!signal) {</span><br><span class=line> <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">Error</span>(<span class=string>"Agent listener invoked outside active run"</span>);</span><br><span class=line> }</span><br><span class=line> <span class=comment>// 串行 await 保证 listener 看到严格事件顺序,同时也会形成背压。</span></span><br><span class=line> <span class=keyword>for</span> (<span class=keyword>const</span> listener <span class=keyword>of</span> <span class="variable language_">this</span>.<span class=property>listeners</span>) {</span><br><span class=line> <span class=keyword>await</span> <span class="title function_">listener</span>(event, signal);</span><br><span class=line> }</span><br><span class=line>}</span><br></pre></table></figure><p>这是一种很清楚的单向数据流:主循环产生事实事件,<code>Agent</code> 根据事件归并状态,TUI 和 Session 再订阅状态变化。主循环不直接操作界面,界面也不用理解 provider 的原始数据格式。<p>还有一个细节:listener 是逐个 <code>await</code> 的。这保证事件顺序稳定,但也意味着某个很慢的 listener 会对整个流形成背压。调试“模型已经返回但界面更新很慢”时,除了查网络层,也要检查事件订阅者。<h2 id=十三、适合实际调试的一组断点><a class=headerlink href=#十三、适合实际调试的一组断点 title=十三、适合实际调试的一组断点></a>十三、适合实际调试的一组断点</h2><p>我最后把断点分成四组,按问题选择,而不是每次全部打开。<h3 id=模型没有出现或认证失败><a class=headerlink href=#模型没有出现或认证失败 title=模型没有出现或认证失败></a>模型没有出现或认证失败</h3><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br><span class=line>8</span><br></pre><td class=code><pre><span class=line>packages/coding-agent/src/core/sdk.ts</span><br><span class=line> ModelRuntime.create(...)</span><br><span class=line> findInitialModel(...)</span><br><span class=line></span><br><span class=line>packages/coding-agent/src/core/model-runtime.ts</span><br><span class=line> create(...)</span><br><span class=line> getAuth(...)</span><br><span class=line> prepareRequest(...)</span><br></pre></table></figure><p>重点观察 <code>agentDir</code>、<code>modelsPath</code>、选中的 <code>model.provider/model.id</code> 和认证来源。不要在调试控制台或截图中暴露真实 key。<h3 id=请求参数、代理地址或-header-不符合预期><a title="请求参数、代理地址或 header 不符合预期" class=headerlink href=#请求参数、代理地址或-header-不符合预期></a>请求参数、代理地址或 header 不符合预期</h3><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>packages/coding-agent/src/core/sdk.ts</span><br><span class=line> streamFn: async (...)</span><br><span class=line> onPayload</span><br><span class=line> transformHeaders</span><br><span class=line></span><br><span class=line>packages/coding-agent/src/core/model-runtime.ts</span><br><span class=line> streamSimple(...)</span><br></pre></table></figure><h3 id=Agent-为什么继续或停止><a title="Agent 为什么继续或停止" class=headerlink href=#Agent-为什么继续或停止></a>Agent 为什么继续或停止</h3><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br></pre><td class=code><pre><span class=line>packages/agent/src/agent-loop.ts</span><br><span class=line> runLoop(...)</span><br></pre></table></figure><p>每次停在循环顶部时观察:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br></pre><td class=code><pre><span class=line>hasMoreToolCalls</span><br><span class=line>pendingMessages.length</span><br><span class=line>lastCompletedTurn</span><br><span class=line>message.stopReason</span><br><span class=line>toolCalls.length</span><br><span class=line>followUpMessages.length</span><br></pre></table></figure><h3 id=工具为什么没有执行><a class=headerlink href=#工具为什么没有执行 title=工具为什么没有执行></a>工具为什么没有执行</h3><figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br></pre><td class=code><pre><span class=line>packages/agent/src/agent-loop.ts</span><br><span class=line> executeToolCalls(...)</span><br><span class=line> prepareToolCall(...)</span><br><span class=line> executePreparedToolCall(...)</span><br><span class=line> finalizeExecutedToolCall(...)</span><br></pre></table></figure><p>重点排查工具不存在、schema 校验失败、<code>beforeToolCall</code> 阻止、AbortSignal 取消和 <code>stopReason === "length"</code> 这几种分支。<h2 id=十四、总结><a class=headerlink href=#十四、总结 title=十四、总结></a>十四、总结</h2><p>Pi Agent 的核心并不是一个很玄学的黑盒。把 UI、会话和 provider 适配暂时拿掉以后,它的主干可以概括为:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br><span class=line>2</span><br><span class=line>3</span><br><span class=line>4</span><br><span class=line>5</span><br><span class=line>6</span><br><span class=line>7</span><br></pre><td class=code><pre><span class=line>准备上下文</span><br><span class=line> → 请求模型并消费流事件</span><br><span class=line> → 得到完整 assistant message</span><br><span class=line> → 如果有工具调用,执行并追加 toolResult</span><br><span class=line> → 带着新上下文再次请求模型</span><br><span class=line> → 当前任务链结束后检查 follow-up</span><br><span class=line> → 发出 agent_end</span><br></pre></table></figure><p>但真正值得学习的地方恰恰是这些“简单主干”周围没有被省略的工程细节:<ul><li>用 <code>StreamFn</code> 做依赖注入,让 Agent Core 与 provider 解耦;<li>用 partial message 原位替换保证流式状态不污染历史;<li>用内外两层循环区分工具链、steering 和 follow-up;<li>工具调用先校验、可拦截、可并行,也能把失败反馈给模型;<li>token 截断时拒绝执行可能不完整的工具参数;<li>API key 每轮动态解析,适配会过期的认证;<li>通过统一事件流连接 Agent 状态、Session 和 TUI。</ul><p>完成这条调用链以后,再继续阅读上下文压缩、Session JSONL、扩展系统和具体 Provider,就不会只是看到一堆互相跳转的 TypeScript 文件,而是能判断每一层在整个 Agent 生命周期中的位置。<h2 id=参考源码><a class=headerlink href=#参考源码 title=参考源码></a>参考源码</h2><ul><li><a href=https://github.com/earendil-works/pi rel=noopener target=_blank>Pi 源码仓库</a><li><a href=https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/coding-agent/src/core/sdk.ts rel=noopener target=_blank><code>packages/coding-agent/src/core/sdk.ts</code></a><li><a href=https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/agent/src/agent.ts rel=noopener target=_blank><code>packages/agent/src/agent.ts</code></a><li><a href=https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/agent/src/agent-loop.ts rel=noopener target=_blank><code>packages/agent/src/agent-loop.ts</code></a><li><a href=https://github.com/earendil-works/pi/blob/71dca871bc80b6bc97be37f0ca3189399d651fff/packages/coding-agent/docs/models.md rel=noopener target=_blank><code>packages/coding-agent/docs/models.md</code></a></ul></div><footer class=post-footer><aside aria-label=文章所属专题 class=post-series><span>所属专题</span><a href=/topics/#agent-engineering>Agent 工程实践</a><p>从 Agent 开发框架和工具系统出发,理解循环、上下文、工具调用、Subagent 与 MCP 的工程实现。</aside><div class="affiliate-creative affiliate-creative--post-end" data-affiliate-id=tencent-workbuddy><a aria-label="腾讯 WorkBuddy" rel="sponsored noopener" href=https://cloud.tencent.com/act/cps/redirect?redirect=6871&cps_key=2e33a1eedb380997d03618b4a5ba6ca7&from=console target=_blank><img alt="腾讯 WorkBuddy AI 办公工作台" decoding=async height=389 loading=lazy src=/uploads/affiliate/tencent-workbuddy.webp width=690> </a><span class=affiliate-disclosure>推广</span></div><div class=reward-container><div>请我一杯咖啡吧!</div><button>赞赏</button><div class=post-reward><div><img alt="Nicksxs 微信" src=https://i.loli.net/2020/01/12/NUlWanT31E8LQGx.png><span>微信</span></div><div><img alt="Nicksxs 支付宝" src=https://ooo.0o0.ooo/2016/08/17/57b421e773057.jpg><span>支付宝</span></div></div></div><div class=post-copyright><ul><li class=post-copyright-author><strong>本文作者: </strong>Nicksxs<li class=post-copyright-link><strong>本文链接:</strong> <a title="Pi Agent 源码解析(一):从调试环境到双层主循环" href=https://nicksxs.me/2026/09/13/Pi-Agent-%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E4%BB%8E%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83%E5%88%B0%E5%8F%8C%E5%B1%82%E4%B8%BB%E5%BE%AA%E7%8E%AF/>https://nicksxs.me/2026/09/13/Pi-Agent-源码解析-从调试环境到双层主循环/</a><li class=post-copyright-license><strong>版权声明: </strong>本博客所有文章除特别声明外,均采用 <a href=https://creativecommons.org/licenses/by-nc-sa/4.0/ rel=noopener target=_blank><i class="fab fa-fw fa-creative-commons"></i>BY-NC-SA</a> 许可协议。转载请注明出处!</ul></div><div class=post-tags><a href=/tags/LLM/ rel=tag># LLM</a><a href=/tags/Agent/ rel=tag># Agent</a><a href=/tags/TypeScript/ rel=tag># TypeScript</a></div><div class=post-nav><div class=post-nav-item><a title="6GB显存能跑35B MoE吗:FreeToken极限配置、性能压榨与实操教程" href=/2026/09/05/6GB%E6%98%BE%E5%AD%98%E8%B7%9135B-MoE-FreeToken%E6%9E%81%E9%99%90%E9%85%8D%E7%BD%AE%E4%B8%8E%E5%AE%9E%E6%93%8D%E6%95%99%E7%A8%8B/ rel=prev><i class="fa fa-angle-left"></i> 6GB显存能跑35B MoE吗:FreeToken极限配置、性能压榨与实操教程</a></div><div class=post-nav-item><a title="Jev 深度解读与实测:只做判断的 AI,究竟快在哪里" href=/2026/09/20/Jev%E6%B7%B1%E5%BA%A6%E8%A7%A3%E8%AF%BB-%E4%B8%BA%E4%BB%80%E4%B9%88%E8%BF%99%E4%B8%AAAI%E5%8F%AA%E5%81%9A%E5%88%A4%E6%96%AD%E5%8D%B4%E8%83%BD%E6%9B%B4%E5%BF%AB/ rel=next>Jev 深度解读与实测:只做判断的 AI,究竟快在哪里 <i class="fa fa-angle-right"></i></a></div></div></footer></article></div><div class=comments id=disqus_thread><noscript>Please enable JavaScript to view the comments powered by Disqus.</noscript></div></div></main><footer class=footer><div class=footer-inner><div class=beian><a href=https://beian.miit.gov.cn/ rel=noopener target=_blank>浙ICP备16002580号-2 </a><img alt src=/uploads/beian.png><a href=https://beian.mps.gov.cn/#/query/webSearch?code=33010602011094 rel=noopener target=_blank>浙公网安备 33010602011094号</a></div><div class=copyright>© <span itemprop=copyrightYear>2026</span><span class=with-love><i class="fa fa-heart"></i> </span><span class=author itemprop=copyrightHolder>Nicksxs</span></div></div></footer><div class="toggle sidebar-toggle" role=button><span class=toggle-line></span><span class=toggle-line></span><span class=toggle-line></span></div><div class=sidebar-dimmer></div><div aria-label=返回顶部 class=back-to-top role=button><i class="fa fa-arrow-up fa-lg"></i><span>0%</span></div><a aria-label="在 GitHub 上关注我" title="在 GitHub 上关注我" class=github-corner href=https://github.com/nicksxs rel=noopener target=_blank><svg viewbox="0 0 250 250" aria-hidden=true height=80 width=80><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" style="transform-origin:130px 106px" class=octo-arm fill=currentColor></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" class=octo-body fill=currentColor></path></svg></a><noscript><div class=noscript-warning>Theme NexT works best with JavaScript enabled</div></noscript><script data-cloudflare-page-counter-config type=application/json>{"enable":true,"endpoint":"https://counter.nicksxs.me","site":"nicksxs","local_cache":true,"timeout":5000,"icon":"far fa-eye","item_text":true}</script><script src=/js/cloudflare-page-counter.js></script><script class=next-config data-name=disqus type=application/json>{"enable":true,"shortname":"nicksxs","count":false,"i18n":{"disqus":"disqus"}}</script><script defer src=/js/third-party/comments/disqus.js></script>
|