You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

1 lines
118 KiB

<!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 1.0 的固定版本源码,分析 MCP 工具的按需发现、QuickJS 代码编排、结果过滤与后台连接,直接对照提示词压缩前后的代码,并说明 OAuth、会话恢复和工具搜索的实现边界。" name=description><meta content=article property=og:type><meta content="Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩" property=og:title><meta content=https://nicksxs.me/2026/10/04/Pi-Agent-1-0-MCP%E9%9B%86%E6%88%90%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%B7%A5%E5%85%B7%E5%8F%91%E7%8E%B0%E4%B8%8E%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%8E%8B%E7%BC%A9/index.html property=og:url><meta content="Nicksxs's Blog" property=og:site_name><meta content="结合 Pi Agent 1.0 的固定版本源码,分析 MCP 工具的按需发现、QuickJS 代码编排、结果过滤与后台连接,直接对照提示词压缩前后的代码,并说明 OAuth、会话恢复和工具搜索的实现边界。" property=og:description><meta content=zh_CN property=og:locale><meta content=2026-10-04T14:30:00.000Z property=article:published_time><meta content=2026-10-04T14:30: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=MCP property=article:tag><meta content=TypeScript property=article:tag><meta content=summary name=twitter:card><link href=https://nicksxs.me/2026/10/04/Pi-Agent-1-0-MCP%E9%9B%86%E6%88%90%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%B7%A5%E5%85%B7%E5%8F%91%E7%8E%B0%E4%B8%8E%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%8E%8B%E7%BC%A9/ 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/10/04/Pi-Agent-1-0-MCP%E9%9B%86%E6%88%90%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%B7%A5%E5%85%B7%E5%8F%91%E7%8E%B0%E4%B8%8E%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%8E%8B%E7%BC%A9/","path":"2026/10/04/Pi-Agent-1-0-MCP集成源码解析-工具发现与提示词压缩/","title":"Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩"}</script><script class=next-config data-name=calendar type=application/json>""</script><title>Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩 | 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=#%E5%85%88%E7%A1%AE%E8%AE%A4%E7%89%88%E6%9C%AC%EF%BC%9AMCP-%E5%9C%A8-0-99-0-%E5%B7%B2%E7%BB%8F%E8%BF%9B%E5%85%A5%E5%86%85%E7%BD%AE%E6%89%A9%E5%B1%95><span class=nav-number>1.</span> <span class=nav-text>先确认版本:MCP 在 0.99.0 已经进入内置扩展</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%B7%A5%E5%85%B7%E5%B7%B2%E7%BB%8F%E6%B3%A8%E5%86%8C%EF%BC%8C%E6%A8%A1%E5%9E%8B%E4%BB%8D%E5%8F%AF%E4%BB%A5%E6%9A%82%E6%97%B6%E7%9C%8B%E4%B8%8D%E5%88%B0%E5%AE%83><span class=nav-number>2.</span> <span class=nav-text>工具已经注册,模型仍可以暂时看不到它</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#searchTools-%E4%B8%8E-tool-search-%E5%81%9A%E4%BA%86%E4%B8%A4%E4%BB%B6%E4%B8%8D%E5%90%8C%E7%9A%84%E4%BA%8B><span class=nav-number>3.</span> <span class=nav-text>searchTools 与 tool_search 做了两件不同的事</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#Codemode-%E6%8A%8A%E5%AE%8C%E6%95%B4%E7%BB%93%E6%9E%9C%E7%95%99%E5%9C%A8%E8%84%9A%E6%9C%AC%E4%B8%AD><span class=nav-number>4.</span> <span class=nav-text>Codemode 把完整结果留在脚本中</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#1-0-%E7%9A%84%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%88%B0%E5%BA%95%E4%BB%8E%E4%BB%80%E4%B9%88%E7%BC%A9%E5%88%B0%E4%BB%80%E4%B9%88><span class=nav-number>5.</span> <span class=nav-text>1.0 的提示词到底从什么缩到什么</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E6%89%A7%E8%A1%8C%E8%A7%84%E5%88%99%E4%B8%8E-helper%EF%BC%9A%E9%80%90%E9%A1%B9%E8%AF%B4%E6%98%8E%E6%94%B9%E6%88%90%E5%88%86%E7%BB%84%E7%9F%AD%E8%AF%B4%E6%98%8E><span class=nav-number>5.1.</span> <span class=nav-text>执行规则与 helper:逐项说明改成分组短说明</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#models-API%EF%BC%9A%E5%AE%8C%E6%95%B4%E7%B1%BB%E5%9E%8B%E5%92%8C%E6%96%B9%E6%B3%95%E5%A3%B0%E6%98%8E%E6%94%B9%E6%88%90%E6%96%87%E6%A1%A3%E5%85%A5%E5%8F%A3><span class=nav-number>5.2.</span> <span class=nav-text>models API:完整类型和方法声明改成文档入口</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E5%B7%B2%E5%A3%B0%E6%98%8E%E5%B7%A5%E5%85%B7%EF%BC%9A%E5%AE%8C%E6%95%B4-TypeScript-%E5%A3%B0%E6%98%8E%E6%94%B9%E6%88%90%E8%B0%83%E7%94%A8%E4%B8%8E%E8%BF%94%E5%9B%9E%E5%AD%97%E6%AE%B5%E9%80%9F%E8%A7%88><span class=nav-number>5.3.</span> <span class=nav-text>已声明工具:完整 TypeScript 声明改成调用与返回字段速览</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#MCP-%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%AE%B5%EF%BC%9A%E5%9B%BA%E5%AE%9A%E8%A7%A3%E9%87%8A%E6%89%80%E6%9C%89%E8%B7%AF%E5%BE%84%E6%94%B9%E6%88%90%E6%8C%89%E5%AE%9E%E9%99%85%E8%B7%AF%E5%BE%84%E7%94%9F%E6%88%90><span class=nav-number>5.4.</span> <span class=nav-text>MCP 服务器段:固定解释所有路径改成按实际路径生成</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E7%B3%BB%E7%BB%9F%E6%8F%90%E7%A4%BA%E4%B8%AD%E7%9A%84%E4%BD%BF%E7%94%A8%E5%BB%BA%E8%AE%AE%E4%B9%9F%E7%BC%A9%E7%9F%AD%E4%BA%86><span class=nav-number>5.5.</span> <span class=nav-text>系统提示中的使用建议也缩短了</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%8F%8F%E8%BF%B0%E4%BF%9D%E6%8C%81%E7%A8%B3%E5%AE%9A%EF%BC%8C%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%9C%A8%E5%90%8E%E5%8F%B0%E8%BF%9E%E6%8E%A5><span class=nav-number>6.</span> <span class=nav-text>描述保持稳定,服务器在后台连接</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#1-0-%E7%9A%84%E7%BA%A0%E9%94%99%E6%8F%90%E7%A4%BA%E8%AE%A9%E6%A8%A1%E5%9E%8B%E6%9C%89%E6%98%8E%E7%A1%AE%E7%9A%84%E6%81%A2%E5%A4%8D%E8%B7%AF%E5%BE%84><span class=nav-number>7.</span> <span class=nav-text>1.0 的纠错提示让模型有明确的恢复路径</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#OAuth%EF%BC%9A%E5%B0%91%E4%B8%80%E4%BA%9B%E5%8F%8D%E5%A4%8D%E7%99%BB%E5%BD%95%EF%BC%8C%E8%B4%A6%E5%8F%B7%E5%92%8C%E6%9D%83%E9%99%90%E4%B9%9F%E8%A6%81%E5%AD%98%E5%AF%B9><span class=nav-number>8.</span> <span class=nav-text>OAuth:少一些反复登录,账号和权限也要存对</span></a><ol class=nav-child><li class="nav-item nav-level-3"><a class=nav-link href=#%E8%A1%A5%E6%9D%83%E9%99%90%E6%97%B6%E4%BF%9D%E7%95%99%E5%B7%B2%E7%BB%8F%E6%8B%BF%E5%88%B0%E7%9A%84%E6%9D%83%E9%99%90><span class=nav-number>8.1.</span> <span class=nav-text>补权限时保留已经拿到的权限</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E5%90%8C%E4%B8%80%E4%B8%AA%E6%9C%8D%E5%8A%A1%E5%9C%B0%E5%9D%80%E5%8F%AF%E4%BB%A5%E4%BF%9D%E7%95%99%E5%A4%9A%E4%B8%AA%E8%B4%A6%E5%8F%B7><span class=nav-number>8.2.</span> <span class=nav-text>同一个服务地址可以保留多个账号</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E6%A3%80%E6%9F%A5%E6%8E%88%E6%9D%83%E5%9B%9E%E8%B0%83%E6%9D%A5%E8%87%AA%E5%93%AA%E4%B8%AA-issuer><span class=nav-number>8.3.</span> <span class=nav-text>检查授权回调来自哪个 issuer</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#Discovery-%E6%9C%89%E9%97%AE%E9%A2%98%E6%97%B6%E5%8F%AF%E4%BB%A5%E6%8C%87%E5%AE%9A-metadata><span class=nav-number>8.4.</span> <span class=nav-text>Discovery 有问题时可以指定 metadata</span></a><li class="nav-item nav-level-3"><a class=nav-link href=#%E7%A9%BA%E5%AD%97%E6%AE%B5%E4%B8%8D%E5%86%8D%E7%A0%B4%E5%9D%8F%E6%95%B4%E6%9D%A1%E7%99%BB%E5%BD%95%E6%B5%81%E7%A8%8B><span class=nav-number>8.5.</span> <span class=nav-text>空字段不再破坏整条登录流程</span></a></ol><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%81%A2%E5%A4%8D%E4%BC%9A%E8%AF%9D%E6%97%B6%EF%BC%8C%E5%85%88%E4%BF%9D%E5%AD%98%E8%BF%98%E6%B2%A1%E6%9C%89%E6%B3%A8%E5%86%8C%E7%9A%84%E5%B7%A5%E5%85%B7%E5%90%8D><span class=nav-number>9.</span> <span class=nav-text>恢复会话时,先保存还没有注册的工具名</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E5%BD%93%E5%89%8D%E5%AE%9E%E7%8E%B0%E8%BF%98%E6%9C%89%E5%93%AA%E4%BA%9B%E8%BE%B9%E7%95%8C><span class=nav-number>10.</span> <span class=nav-text>当前实现还有哪些边界</span></a><li class="nav-item nav-level-2"><a class=nav-link href=#%E6%8C%89%E4%BD%BF%E7%94%A8%E6%96%B9%E5%BC%8F%E9%80%89%E6%8B%A9%E5%B7%A5%E5%85%B7%E6%9B%9D%E5%85%89><span class=nav-number>11.</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/10/04/Pi-Agent-1-0-MCP%E9%9B%86%E6%88%90%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%B7%A5%E5%85%B7%E5%8F%91%E7%8E%B0%E4%B8%8E%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%8E%8B%E7%BC%A9/ 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 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩 | Nicksxs's Blog" itemprop=name><meta content="结合 Pi Agent 1.0 的固定版本源码,分析 MCP 工具的按需发现、QuickJS 代码编排、结果过滤与后台连接,直接对照提示词压缩前后的代码,并说明 OAuth、会话恢复和工具搜索的实现边界。" itemprop=description></span><header class=post-header><h1 itemprop="name headline" class=post-title>Pi Agent 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩</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-10-04 22:30:00" datetime=2026-10-04T22:30:00+08:00>2026-10-04</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 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩" data-increment=true data-path=/2026/10/04/Pi-Agent-1-0-MCP%E9%9B%86%E6%88%90%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%B7%A5%E5%85%B7%E5%8F%91%E7%8E%B0%E4%B8%8E%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%8E%8B%E7%BC%A9/ 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 1.0 的固定版本源码,分析 MCP 工具的按需发现、QuickJS 代码编排、结果过滤与后台连接,直接对照提示词压缩前后的代码,并说明 OAuth、会话恢复和工具搜索的实现边界。</div></div></header><div class=post-body itemprop=articleBody><p>给 Agent 接上几个 MCP 服务器以后,很容易碰到一个问题:还没真正调用工具,请求里就已经带上了一大批工具名字、参数和使用说明。等工具开始返回数据,一份很长的 JSON 又进入上下文,后面的模型请求还要继续带着它。<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 的工具系统</a>和 <a href=/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/>Pi Agent 的双层主循环</a>。这次继续看 2026 年 10 月 1 日发布的 Pi 1.0,重点是 MCP 工具怎样进入模型请求,以及 Pi 怎样减少这部分上下文开销。<p>MCP(Model Context Protocol)约定了客户端怎样连接服务器、发现工具、调用工具和读取资源。接入协议以后,Agent 还要决定哪些工具提前告诉模型、工具调用怎样编排、结果保留多少。Pi 这次改动主要落在这些位置。<p>官方提到 Codemode 的提示词约减少 40%。这个数字很容易让人想知道:到底删了什么,原来的提示词是什么样,缩短后模型又靠什么找到工具?下文会把改前、改后的代码直接放在一起。</p><span id=more></span><h2 id=先确认版本:MCP-在-0-99-0-已经进入内置扩展><a title="先确认版本:MCP 在 0.99.0 已经进入内置扩展" class=headerlink href=#先确认版本:MCP-在-0-99-0-已经进入内置扩展></a>先确认版本:MCP 在 0.99.0 已经进入内置扩展</h2><p>Pi 1.0 的发布文章把 MCP、Codemode 和延迟工具加载放在一起介绍。按具体 tag 的变更记录看,这套能力分几次进入仓库:<table><thead><tr><th>版本<th>日期<th>MCP 相关变化<tbody><tr><td>0.99.0<td>2026-09-29<td>首次内置 MCP、Codemode 和 <code>tool_search</code><tr><td>0.99.2<td>2026-09-30<td>默认 MCP 工具按需发现;工具描述不再随服务器连接变化;首次请求只等待 direct 服务器<tr><td>1.0.0<td>2026-10-01<td>进一步压缩固定提示词,增加纠错提示,修复 OAuth 和恢复已加载工具的问题</table><p>版本归属可以在<a href=https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/CHANGELOG.md#L1-L100 rel=noopener target=_blank>固定到 1.0.0 的 CHANGELOG</a> 中核对。官方 <a href=https://earendil.com/posts/you-said-no-mcp/ rel=noopener target=_blank>“You Said No MCP!”</a> 也解释了接入思路:工具需要容易发现、能够返回结构化数据,并能像函数一样组合调用。<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></pre><td class=code><pre><span class=line>repository: https://github.com/earendil-works/pi</span><br><span class=line>tag: v1.0.0</span><br><span class=line>commit: a13d35a742c6ef8462812a28fbe1d8c8b7431c32</span><br><span class=line></span><br><span class=line>提示词压缩的具体对照:</span><br><span class=line>before: 88ff80b986e34d4fbd1fa94a4df65c60ae964516</span><br><span class=line>after: 6f1072cc081f06b86a673bd142f03720d17afe15</span><br></pre></table></figure><p><code>before</code> 是提示词压缩 commit 的父版本。它已经包含图像生成 API,因此不能拿 0.99.2 的整段提示词直接替代这个对照。本文后面的压缩代码来自该 commit 前后;其余实现以 1.0.0 为准。<p>本文根据源码、commit diff 和仓库测试断言分析。没有运行真实 MCP 服务或复现官方的 provider token 样本。字符数对照来自提示字符串统计,官方 token 数会单独标明。<h2 id=工具已经注册,模型仍可以暂时看不到它><a class=headerlink href=#工具已经注册,模型仍可以暂时看不到它 title=工具已经注册,模型仍可以暂时看不到它></a>工具已经注册,模型仍可以暂时看不到它</h2><p>过去比较直接的做法,是连接服务器、拿到工具列表,然后把所有工具 schema 都交给模型。schema 在这里指输入参数及其类型、必填字段等说明。<p>Pi 为工具增加了不同的 exposure,也就是模型通过什么路径接触工具:<table><thead><tr><th>exposure<th>默认行为<th>适合的使用方式<tbody><tr><td><code>codemode</code><td>MCP 默认值;工具可从脚本调用,完整声明不预先进入模型请求<td>在脚本里组合、并行或筛选 MCP 调用<tr><td><code>deferred</code><td>工具被 <code>tool_search</code> 找到后,在下一次模型请求中声明<td>按需加载后直接调用<tr><td><code>direct</code><td>工具直接声明给模型,也可以从 Codemode 调用<td>少量经常使用的工具<tr><td><code>hidden</code><td>工具不可调用<td>关闭某些服务器或工具能力</table><p>服务器级的 <code>exposure</code> 还可以被单个工具的 <code>toolExposure</code> 覆盖。比如把查询工具交给 Codemode,把某个常用工具直接声明,把不需要的工具隐藏。<a href=https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/mcp.md#L141-L175 rel=noopener target=_blank>固定版本配置说明</a><p>这里还有一层名称转换:MCP 配置中的 <code>codemode</code> 会被映射为通用工具层的 <code>deferred</code>。两种 MCP 配置都不会把完整声明预先放进 Codemode description;MCP 扩展再根据配置决定激活 Codemode 还是 <code>tool_search</code>,供模型发现这些工具:<figure class="highlight ts"><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=keyword>export</span> <span class=keyword>function</span> <span class="title function_">toToolExposure</span>(<span class=params>exposure: McpExposure</span>): <span class="title class_">ToolExposure</span> {</span><br><span class=line> <span class=keyword>return</span> exposure === <span class=string>"codemode"</span> ? <span class=string>"deferred"</span> : exposure;</span><br><span class=line>}</span><br></pre></table></figure><p>因此,下面通用工具层的 <code>codemode</code> 分支与 MCP 配置中的同名值处在不同层次。默认 MCP 工具在这一步落入 <code>deferred</code> 分支。源码见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/mcp/tools.ts#L35-L41 rel=noopener target=_blank>toToolExposure</a>。<p>产品层把已注册工具、脚本可调用工具和模型直接看到的工具分开。<code>AgentSession._getCallableTools()</code> 负责计算脚本可调用的集合:<figure class="highlight ts"><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=keyword>private</span> <span class="title function_">_getCallableTools</span>(<span class=attr>active</span>: <span class="title class_">ReadonlySet</span><<span class=built_in>string</span>> = <span class=keyword>new</span> <span class="title class_">Set</span>(<span class="variable language_">this</span>.<span class="title function_">getActiveToolNames</span>())): <span class="title class_">AgentTool</span>[] {</span><br><span class=line> <span class=keyword>return</span> [...<span class="variable language_">this</span>.<span class=property>_toolRegistry</span>.<span class="title function_">values</span>()].<span class="title function_">filter</span>(<span class=function>(<span class=params>tool</span>) =></span> {</span><br><span class=line> <span class=keyword>const</span> exposure = <span class="variable language_">this</span>.<span class="title function_">_getToolExposure</span>(tool.<span class=property>name</span>);</span><br><span class=line> <span class=keyword>return</span> exposure === <span class=string>"codemode"</span> || exposure === <span class=string>"deferred"</span> || (exposure === <span class=string>"direct"</span> && active.<span class="title function_">has</span>(tool.<span class=property>name</span>));</span><br><span class=line> });</span><br><span class=line> }</span><br></pre></table></figure><p><code>codemode</code> 和 <code>deferred</code> 工具只要注册完成,就能走脚本调用路径。<code>direct</code> 工具还要求它在 active tool 集合中。这样就可以把大量工具留在运行时,同时控制进入模型请求的声明数量。源码见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/core/agent-session.ts#L1511-L1520 rel=noopener target=_blank>agent-session.ts</a>。<h2 id=searchTools-与-tool-search-做了两件不同的事><a title="searchTools 与 tool_search 做了两件不同的事" class=headerlink href=#searchTools-与-tool-search-做了两件不同的事></a>searchTools 与 tool_search 做了两件不同的事</h2><p>默认不把所有 schema 塞进请求以后,模型需要有地方查工具。Pi 提供了脚本内查询和模型工具加载两条路径。<p><code>searchTools()</code> 是 Codemode 脚本中的查询函数。它搜索可调用工具,返回匹配项的名称和描述;这里的描述实际包含生成后的 TypeScript 声明。因此,它能按需拿到参数结构。调用方式可以写成下面这样,<code>mcp__tracker</code> 只是示例命名空间:<figure class="highlight js"><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="title function_">text</span>(<span class=keyword>await</span> <span class="title function_">searchTools</span>(<span class=string>"search issues"</span>, {</span><br><span class=line> <span class=attr>namespace</span>: <span class=string>"mcp__tracker"</span>,</span><br><span class=line> <span class=attr>limit</span>: <span class=number>3</span>,</span><br><span class=line>}));</span><br><span class=line></span><br><span class=line><span class="title function_">text</span>(<span class=keyword>await</span> <span class="title function_">describeNamespace</span>(<span class=string>"mcp__tracker"</span>));</span><br></pre></table></figure><p><code>describeNamespace()</code> 返回服务器的 instructions 和工具名,<code>describeTool()</code> 按具体名称返回工具描述及声明。这些数据先进入脚本,只有通过 <code>text()</code> 或 <code>return</code> 输出的部分才会进入模型上下文。源码见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/codemode/execute.ts#L451-L513 rel=noopener target=_blank>createDiscoveryGlobals</a>。<p><code>tool_search</code> 则是模型可以直接调用的工具。它查找尚未加载的工具,将匹配项加入 active tools,从下一次模型请求开始声明。已经加载的工具会记录在会话中。源码见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/tool-search/tool.ts#L186-L213 rel=noopener target=_blank>searchAndLoad</a>。<p>这两个接口的区别会影响上下文:脚本里查询并调用工具,可以一直把处理留在脚本中;通过 <code>tool_search</code> 加载以后,模型会开始直接接收匹配工具的声明。<p>当前内置检索器采用 BM25,即根据查询词和工具说明中的词项匹配程度排序。检索内容包括工具名、description、输入 schema 的属性名和说明,以及 namespace 的描述与 instructions。Codemode 的 <code>searchTools()</code> 默认返回 8 个匹配项。<a href=https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/src/extensions/tool-search/tool.ts#L103-L155 rel=noopener target=_blank>检索实现</a><h2 id=Codemode-把完整结果留在脚本中><a title="Codemode 把完整结果留在脚本中" class=headerlink href=#Codemode-把完整结果留在脚本中></a>Codemode 把完整结果留在脚本中</h2><p>Codemode 让模型写一段 JavaScript,再在 QuickJS 沙箱中运行。QuickJS 是一个可以编译成 WebAssembly 的 JavaScript 引擎;脚本没有 Node API、文件系统、网络或计时器,需要通过注入的工具访问外部能力。<p>一次 MCP 调用的路径可以按下面的顺序读:<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>模型生成 JavaScript</span><br><span class=line> → QuickJS 运行脚本</span><br><span class=line> → tools.&LTname>(args)</span><br><span class=line> → ctx.executeTool()</span><br><span class=line> → MCP 服务器</span><br><span class=line> → 完整结果回到脚本</span><br><span class=line> → 脚本筛选、聚合</span><br><span class=line> → text() / return 输出给模型</span><br></pre></table></figure><p><code>executeCodemode()</code> 没有直接绕过产品层调用 MCP 客户端。它把嵌套调用交给 <code>ctx.executeTool()</code>。下面是其中连续的调用片段:<figure class="highlight ts"><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=keyword>const</span> outcome = <span class=keyword>await</span> ctx.<span class="title function_">executeTool</span>(tool.<span class=property>name</span>, args, { <span class=attr>signal</span>: callSignal });</span><br><span class=line>record.<span class=property>id</span> = outcome.<span class=property>toolCall</span>.<span class=property>id</span>;</span><br><span class=line>record.<span class=property>durationMs</span> = performance.<span class="title function_">now</span>() - callStartedAt;</span><br></pre></table></figure><p>这让嵌套调用继续经过正常工具流水线,参数校验和扩展的 <code>tool_call</code>、<code>tool_result</code> 回调仍然生效。配置了权限扩展时,扩展也能拦截这些调用。最终发送给模型的是脚本输出,并不会自动把每个嵌套 MCP 结果全文放进对话。源码见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/codemode/execute.ts#L345-L432 rel=noopener target=_blank>executeCodemode</a>。<p>例如,需要查询一批 issue,再读取各自的评论,可以在脚本里并行取数据、筛选、排序,只把需要的条目交给模型。这是用于说明机制的场景,本文没有对它测量耗时或 token。<p>MCP 结果进入产品层以后,<code>convertMcpResult()</code> 会生成两种视图:模型可以看的 content,以及脚本可以拿到的完整结构化结果。下面是源码:<figure class="highlight ts"><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>export</span> <span class=keyword>async</span> <span class=keyword>function</span> <span class="title function_">convertMcpResult</span>(<span class=params></span></span><br><span class=line><span class=params> server: <span class=built_in>string</span>,</span></span><br><span class=line><span class=params> tool: <span class=built_in>string</span>,</span></span><br><span class=line><span class=params> result: CallToolResult,</span></span><br><span class=line><span class=params> options: ConvertMcpResultOptions = {},</span></span><br><span class=line><span class=params></span>): <span class="title class_">Promise</span><<span class="title class_">AgentToolResult</span><<span class="title class_">McpToolDetails</span>>> {</span><br><span class=line> <span class=comment>// Without content blocks, toLlmContent falls back to the structured content as JSON.</span></span><br><span class=line> <span class=keyword>const</span> <span class=attr>converted</span>: (<span class="title class_">TextContent</span> | <span class="title class_">ImageContent</span>)[] =</span><br><span class=line> result.<span class=property>content</span>.<span class=property>length</span> > <span class=number>0</span> ? <span class=keyword>await</span> <span class="title function_">toModelContent</span>(server, result.<span class=property>content</span>, options) : <span class="title function_">toLlmContent</span>(result);</span><br><span class=line> <span class=keyword>if</span> (result.<span class=property>isError</span> && <span class="title function_">textOf</span>(converted) === <span class=string>""</span>) {</span><br><span class=line> converted.<span class="title function_">push</span>({ <span class=attr>type</span>: <span class=string>"text"</span>, <span class=attr>text</span>: <span class=string>`MCP tool <span class=subst>${server}</span>/<span class=subst>${tool}</span> returned an error`</span> });</span><br><span class=line> }</span><br><span class=line> <span class=keyword>const</span> { content, fullOutputPath } = <span class=keyword>await</span> <span class="title function_">limitMcpContent</span>(converted, options.<span class=property>saveOutput</span>);</span><br><span class=line> <span class=keyword>const</span> { <span class=attr>_meta</span>: _ignored, ...scriptResult } = result;</span><br><span class=line> <span class=keyword>return</span> {</span><br><span class=line> content,</span><br><span class=line> <span class=attr>details</span>: { server, tool, ...(fullOutputPath ? { fullOutputPath } : {}) },</span><br><span class=line> <span class=attr>structuredContent</span>: scriptResult <span class=keyword>as</span> <span class=built_in>unknown</span> <span class=keyword>as</span> <span class="title class_">JsonValue</span>,</span><br><span class=line> ...(result.<span class=property>isError</span> ? { <span class=attr>isError</span>: <span class=literal>true</span> } : {}),</span><br><span class=line> };</span><br><span class=line>}</span><br></pre></table></figure><p><code>limitMcpContent()</code> 限制直接给模型的文本,默认上限为 20KB,超出后保留首尾、把完整文本写到临时文件。<code>structuredContent</code> 则保留服务器的 <code>CallToolResult</code>,只移除 <code>_meta</code>。源码见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/mcp/tools.ts#L46 rel=noopener target=_blank>MCP 结果转换</a>。<p>因此,脚本能拿到 <code>content</code>、<code>structuredContent</code> 和 <code>isError</code>,先处理数据再输出。服务器能提供结构化字段时,这种写法比较容易组合;如果结果只有一大段自然语言,脚本还需要自己解析它。<p>这里有一个实际写脚本时要留意的返回规则:<figure class="highlight ts"><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>function</span> <span class="title function_">toScriptValue</span>(<span class=params>tool: AgentTool<<span class=built_in>any</span>>, outcome: AgentToolCallOutcome</span>): <span class=built_in>unknown</span> {</span><br><span class=line> <span class=keyword>const</span> { result } = outcome;</span><br><span class=line> <span class=keyword>if</span> (tool.<span class=property>outputSchema</span> && result.<span class=property>structuredContent</span> !== <span class=literal>undefined</span>) <span class=keyword>return</span> result.<span class=property>structuredContent</span>;</span><br><span class=line> <span class=keyword>const</span> text = <span class="title function_">textOf</span>(result);</span><br><span class=line> <span class=keyword>if</span> (outcome.<span class=property>isError</span>) <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">Error</span>(text || <span class=string>`Tool "<span class=subst>${tool.name}</span>" failed`</span>);</span><br><span class=line> <span class=keyword>return</span> text;</span><br><span class=line>}</span><br></pre></table></figure><p><code>toScriptValue()</code> 先返回有 output schema 的 <code>structuredContent</code>,再判断其他结果是否失败。MCP 的 <code>isError: true</code> 因而仍可能正常 resolve。脚本需要检查 <code>result.isError</code>;<code>Promise.allSettled()</code> 只能接住真正的 rejection,不能替代这个判断。源码见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/codemode/execute.ts#L302-L313 rel=noopener target=_blank>toScriptValue</a>。<h2 id=1-0-的提示词到底从什么缩到什么><a title="1.0 的提示词到底从什么缩到什么" class=headerlink href=#1-0-的提示词到底从什么缩到什么></a>1.0 的提示词到底从什么缩到什么</h2><p>官方给出的整体样本是:默认工具加 Codemode 开启时,GPT-5.6 请求约从 5300 降到 3300 prompt tokens。用这两个约数计算,减少约 37.7%,发布说明概括为约 40%。这个样本描述请求中的提示词,不能直接推成所有 MCP 任务的总费用或耗时都下降 40%。<p>对应 commit 是 <a href=https://github.com/earendil-works/pi/commit/6f1072cc081f06b86a673bd142f03720d17afe15 rel=noopener target=_blank><code>6f1072c</code></a>。它主要修改提示词的构造方式:合并重复解释,复用已有工具 schema,把完整 API 参考移到按需读取的文档。<h3 id=执行规则与-helper:逐项说明改成分组短说明><a title="执行规则与 helper:逐项说明改成分组短说明" class=headerlink href=#执行规则与-helper:逐项说明改成分组短说明></a>执行规则与 helper:逐项说明改成分组短说明</h3><p>改前的 <code>DESCRIPTION_INTRO</code> 包含 13 条执行规则和 11 条 helper 解释,后面还有独立的工具发现说明。以下是改前源码原文:<figure class="highlight ts"><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>const</span> <span class="variable constant_">DESCRIPTION_INTRO</span> = <span class=string>`Run JavaScript code to orchestrate/compose tool calls</span></span><br><span class=line><span class=string>- Evaluates the provided JavaScript code in a fresh QuickJS sandbox as the body of an async function: top-level \`await\` and \`return\` work.</span></span><br><span class=line><span class=string>- All nested tools are available on the global \`tools\` object, for example \`await tools.read(...)\`. Tool names are exposed as normalized JavaScript identifiers, for example \`await tools.mcp__ologs__get_profile(...)\`.</span></span><br><span class=line><span class=string>- Nested tool methods take an object as their input argument.</span></span><br><span class=line><span class=string>- Nested tools return either an object or a string, based on the description.</span></span><br><span class=line><span class=string>- A nested tool call that fails, is blocked, or gets invalid arguments rejects with an Error carrying the tool's error text.</span></span><br><span class=line><span class=string>- Runs raw JavaScript -- no Node, no file system, no network access, no timers.</span></span><br><span class=line><span class=string>- Accepts raw JavaScript source text, not JSON, quoted strings, or markdown code fences.</span></span><br><span class=line><span class=string>- You may optionally start the tool input with a first line like \`// @options: {"max_output_tokens": 1000, "timeout_ms": 60000}\`.</span></span><br><span class=line><span class=string>- \`max_output_tokens\` sets the token budget for the script's output. Defaults to 10000 tokens.</span></span><br><span class=line><span class=string>- \`timeout_ms\` sets a hard deadline for the whole script. By default there is none.</span></span><br><span class=line><span class=string>- When the JS code is fully evaluated, calls that are still running are cancelled and unawaited promises are silently discarded.</span></span><br><span class=line><span class=string>- Tool calls are real and have side effects. If the script fails partway, earlier calls are not undone.</span></span><br><span class=line><span class=string>- Scripts have a 256 MB memory limit; exceeding it throws \`InternalError: out of memory\`. Filter or aggregate large data instead of accumulating it.</span></span><br><span class=line><span class=string></span></span><br><span class=line><span class=string>- Global helpers:</span></span><br><span class=line><span class=string>- \`exit()\`: Immediately ends the current script successfully (like an early return from the top level).</span></span><br><span class=line><span class=string>- \`text(value: string | number | boolean | undefined | null)\`: Appends a text item. Non-string values are stringified with \`JSON.stringify(...)\` when possible.</span></span><br><span class=line><span class=string>- \`image(imageUrlOrItem: string | { image_url: string } | ImageContent)\`: Appends an image item. \`image_url\` should be a base64-encoded \`data:\` URL. To forward an MCP tool image, pass an individual \`ImageContent\` block from \`result.content\`, for example \`image(result.content[0])\`.</span></span><br><span class=line><span class=string>- \`store(key: string, value: any)\`: stores a serializable value under a string key for later \`codemode\` calls in the same session. Storing \`undefined\` deletes the key. Writes are kept only if the script succeeds.</span></span><br><span class=line><span class=string>- \`load(key: string)\`: returns the stored value for a string key, or \`undefined\` if it is missing.</span></span><br><span class=line><span class=string>- \`ALL_TOOLS\`: metadata for the enabled nested tools as \`{ name, description }\` entries.</span></span><br><span class=line><span class=string>- \`searchTools(query: string, options?: { limit?: number; namespace?: string })\`: resolves to the nested tools that best match the query (BM25, default limit 8), as \`{ name, description }\` entries like \`ALL_TOOLS\`.</span></span><br><span class=line><span class=string>- \`describeTool(name: string)\`: resolves to the description and declaration of a nested tool, or \`undefined\`.</span></span><br><span class=line><span class=string>- \`describeNamespace(name: string)\`: resolves to \`{ name, description?, instructions?, tools }\` for a namespace of nested tools, such as an MCP server: its usage instructions and the names of its tools, or \`undefined\`.</span></span><br><span class=line><span class=string>- \`console.log(...)\` and the other \`console\` methods append a text item like \`text()\`.</span></span><br><span class=line><span class=string>- \`return value\` at the top level appends the value like \`text()\`.`</span>;</span><br><span class=line></span><br><span class=line><span class=keyword>const</span> <span class="variable constant_">DEFERRED_TOOLS_GUIDANCE</span> = <span class=string>`Some nested tools may be omitted from this description, such as deferred tools and MCP tools. They are still available on the global \`tools\` object and listed in \`ALL_TOOLS\`.</span></span><br><span class=line><span class=string>To find one, call \`await searchTools(query)\` (pass \`{ namespace }\` to search one namespace), or filter \`ALL_TOOLS\` by \`name\` and \`description\`. \`await describeNamespace(name)\` returns a namespace's usage instructions and the names of its tools.`</span>;</span><br></pre></table></figure><p>改后,执行方式被合并成一段和两条说明,helper 合并成输出、存储和工具发现三组。启用 models 时,再加一条文档指引:<figure class="highlight ts"><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>const</span> <span class="variable constant_">DESCRIPTION_INTRO</span> = <span class=string>`Run JavaScript that calls other tools. The input is raw JavaScript (not JSON, no code fence), run as an async function body in a QuickJS sandbox: top-level \`await\` and \`return\` work. No Node, file system, network, or timers.</span></span><br><span class=line><span class=string>- \`await tools.&LTname>({ ...args })\` resolves to a string, or an object if the tool's declaration says so, and rejects with an Error on failure. Calls still running when the script ends are cancelled.</span></span><br><span class=line><span class=string>- Optional first line: \`// @options: {"max_output_tokens": 10000, "timeout_ms": 60000}\``</span>;</span><br><span class=line></span><br><span class=line><span class=comment>/** One line per global. The details live in {<span class=doctag>@link</span> CODEMODE_DOCS_PATH}. */</span></span><br><span class=line><span class=keyword>function</span> <span class="title function_">describeGlobals</span>(<span class=params>models: <span class=built_in>boolean</span></span>): <span class=built_in>string</span> {</span><br><span class=line> <span class=keyword>const</span> lines = [</span><br><span class=line> <span class=string>"Globals:"</span>,</span><br><span class=line> <span class=string>"- `text(value)`, `image(dataUrlOrImageBlock)`, `console.log(...)`, and top-level `return` add output; `exit()` ends the script."</span>,</span><br><span class=line> <span class=string>"- `store(key, value)` and `load(key)` keep JSON values across codemode calls."</span>,</span><br><span class=line> <span class=string>"- `ALL_TOOLS`, `searchTools(query, { limit?, namespace? })`, `describeTool(name)`, `describeNamespace(name)`: find unlisted tools, such as MCP tools."</span>,</span><br><span class=line> ];</span><br><span class=line> <span class=keyword>if</span> (models) {</span><br><span class=line> lines.<span class="title function_">push</span>(<span class=string>`- \`models\`: classifiers and image generation. Read <span class=subst>${CODEMODE_DOCS_PATH}</span> first.`</span>);</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> lines.<span class="title function_">join</span>(<span class=string>"\n"</span>);</span><br><span class=line>}</span><br></pre></table></figure><p>这里可以直接看到缩短的内容:<code>text()</code>、<code>image()</code>、<code>console.log()</code> 和 <code>return</code> 合在一起,<code>store()</code> 与 <code>load()</code> 合在一起,几种工具发现方法合在一起。关于内存上限、图片格式、store 写入规则、超时默认值等细节,则放到 <code>codemode.md</code> 中。<p>对运行时说明字符串统计,改前的 intro 加独立工具发现说明为 3619 字符;改后的 intro 加不含 models 行的 globals 为 878 字符。统计包含两段之间的换行,不包含 models API、嵌套工具声明和共享 MCP 类型,也没有把源码里的反斜杠转义计作运行时字符。这个数字只说明该段文本的变化。<a href=https://github.com/earendil-works/pi/blob/88ff80b986e34d4fbd1fa94a4df65c60ae964516/packages/coding-agent/src/extensions/codemode/tool.ts#L132-L249 rel=noopener target=_blank>改前源码</a>、<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/codemode/tool.ts#L135-L150 rel=noopener target=_blank>改后源码</a><h3 id=models-API:完整类型和方法声明改成文档入口><a title="models API:完整类型和方法声明改成文档入口" class=headerlink href=#models-API:完整类型和方法声明改成文档入口></a>models API:完整类型和方法声明改成文档入口</h3><p>改前,Codemode 的常驻 description 还会注入完整模型 API。为了看清被移走的具体内容,下面直接列出原来的类型和方法元数据:<figure class="highlight ts"><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></pre><td class=code><pre><span class=line><span class=keyword>const</span> <span class="variable constant_">MODEL_TYPES</span> = <span class=string>`type ModelType = "chat" | "image" | "classifier";</span></span><br><span class=line><span class=string>/** A model catalog entry. \`provider\` and \`id\` identify it; the other fields depend on the type. */</span></span><br><span class=line><span class=string>interface ModelInfo {</span></span><br><span class=line><span class=string> type?: ModelType;</span></span><br><span class=line><span class=string> provider: string;</span></span><br><span class=line><span class=string> id: string;</span></span><br><span class=line><span class=string> name: string;</span></span><br><span class=line><span class=string> api: string;</span></span><br><span class=line><span class=string> input: ("text" | "image")[];</span></span><br><span class=line><span class=string> contextWindow?: number;</span></span><br><span class=line><span class=string> [key: string]: unknown;</span></span><br><span class=line><span class=string>}</span></span><br><span class=line><span class=string>type ClassifierQuestion =</span></span><br><span class=line><span class=string> | { type: "choice"; instructions: string; criteria: Record&LTstring, string> }</span></span><br><span class=line><span class=string> | { type: "score"; instructions: string; criteria: string[] }</span></span><br><span class=line><span class=string> | { type: "bool"; instructions: string; criteria: { true: string; false: string } };</span></span><br><span class=line><span class=string>type ClassifierAnswer =</span></span><br><span class=line><span class=string> | { type: "choice"; choice: string; probabilities: Record&LTstring, number>; confidence: number }</span></span><br><span class=line><span class=string> | { type: "score"; score: number; confidence: number }</span></span><br><span class=line><span class=string> | { type: "bool"; probability: number };</span></span><br><span class=line><span class=string>interface ClassifierContext {</span></span><br><span class=line><span class=string> state: Record&LTstring, unknown>;</span></span><br><span class=line><span class=string> questions: Record&LTstring, ClassifierQuestion>;</span></span><br><span class=line><span class=string>}</span></span><br><span class=line><span class=string>/** Token counts reported by the service. Cost is in USD. */</span></span><br><span class=line><span class=string>type ModelUsage = { input: number; output: number; totalTokens: number; cost: { total: number } };</span></span><br><span class=line><span class=string>interface ClassifierResult {</span></span><br><span class=line><span class=string> api: string;</span></span><br><span class=line><span class=string> provider: string;</span></span><br><span class=line><span class=string> model: string;</span></span><br><span class=line><span class=string> answers: Record&LTstring, ClassifierAnswer>;</span></span><br><span class=line><span class=string> usage?: ModelUsage;</span></span><br><span class=line><span class=string> stopReason: "stop" | "error" | "aborted";</span></span><br><span class=line><span class=string> errorMessage?: string;</span></span><br><span class=line><span class=string> timestamp: number;</span></span><br><span class=line><span class=string>}</span></span><br><span class=line><span class=string>type ModelTextBlock = { type: "text"; text: string };</span></span><br><span class=line><span class=string>/** \`data\` is base64. Show it with \`image(block)\`; never print \`data\` with \`text()\`, \`console\`, or \`return\`. */</span></span><br><span class=line><span class=string>type ModelImageBlock = { type: "image"; data: string; mimeType: string };</span></span><br><span class=line><span class=string>interface ImagesContext {</span></span><br><span class=line><span class=string> /** The prompt as text blocks, plus image blocks to edit or use as references. */</span></span><br><span class=line><span class=string> input: (ModelTextBlock | ModelImageBlock)[];</span></span><br><span class=line><span class=string>}</span></span><br><span class=line><span class=string>interface ImagesResult {</span></span><br><span class=line><span class=string> api: string;</span></span><br><span class=line><span class=string> provider: string;</span></span><br><span class=line><span class=string> model: string;</span></span><br><span class=line><span class=string> /** Generated images, and text blocks for models that also return text. */</span></span><br><span class=line><span class=string> output: (ModelTextBlock | ModelImageBlock)[];</span></span><br><span class=line><span class=string> usage?: ModelUsage;</span></span><br><span class=line><span class=string> stopReason: "stop" | "error" | "aborted";</span></span><br><span class=line><span class=string> errorMessage?: string;</span></span><br><span class=line><span class=string> timestamp: number;</span></span><br><span class=line><span class=string>}`</span>;</span><br><span class=line></span><br><span class=line><span class=comment>/** Declarations of the `models` globals; codemode-execute.ts implements them. */</span></span><br><span class=line><span class=keyword>export</span> <span class=keyword>const</span> <span class=attr>MODEL_GLOBAL_DECLARATIONS</span>: <span class=keyword>readonly</span> <span class="title class_">Omit</span><<span class="title class_">CodemodeTool</span>, <span class=string>"execute"</span>>[] = [</span><br><span class=line> {</span><br><span class=line> <span class=attr>name</span>: <span class=string>"models.getModelsOfType"</span>,</span><br><span class=line> <span class=attr>description</span>: <span class=string>"Every known model of a type, optionally for one provider."</span>,</span><br><span class=line> <span class=attr>signature</span>: <span class=string>"(type: ModelType, provider?: string): Promise&LTModelInfo[]>"</span>,</span><br><span class=line> },</span><br><span class=line> {</span><br><span class=line> <span class=attr>name</span>: <span class=string>"models.getAvailableOfType"</span>,</span><br><span class=line> <span class=attr>description</span>: <span class=string>"Models of a type whose provider has working credentials."</span>,</span><br><span class=line> <span class=attr>signature</span>: <span class=string>"(type: ModelType, provider?: string): Promise&LTModelInfo[]>"</span>,</span><br><span class=line> },</span><br><span class=line> {</span><br><span class=line> <span class=attr>name</span>: <span class=string>"models.getModelOfType"</span>,</span><br><span class=line> <span class=attr>description</span>: <span class=string>"One catalog entry, or undefined."</span>,</span><br><span class=line> <span class=attr>signature</span>: <span class=string>"(type: ModelType, provider: string, id: string): Promise&LTModelInfo | undefined>"</span>,</span><br><span class=line> },</span><br><span class=line> {</span><br><span class=line> <span class=attr>name</span>: <span class=string>"models.classify"</span>,</span><br><span class=line> <span class=attr>description</span>:</span><br><span class=line> <span class=string>"Run a classifier model on one state. Only `provider` and `id` of `model` are used. Provider errors do not throw: check `stopReason` and `errorMessage`."</span>,</span><br><span class=line> <span class=attr>signature</span>: <span class=string>"(model: ModelInfo, context: ClassifierContext): Promise&LTClassifierResult>"</span>,</span><br><span class=line> },</span><br><span class=line> {</span><br><span class=line> <span class=attr>name</span>: <span class=string>"models.generateImages"</span>,</span><br><span class=line> <span class=attr>description</span>:</span><br><span class=line> <span class=string>'Generate images with an image model. Only `provider` and `id` of `model` are used. Provider errors do not throw: check `stopReason` and `errorMessage`. Generation can take minutes, so do not set a short `timeout_ms`. Show results with `for (const block of result.output) if (block.type === "image") image(block);`.'</span>,</span><br><span class=line> <span class=attr>signature</span>: <span class=string>"(model: ModelInfo, context: ImagesContext): Promise&LTImagesResult>"</span>,</span><br><span class=line> },</span><br><span class=line>];</span><br></pre></table></figure><p>这些声明通过下面的代码进入 description:<figure class="highlight ts"><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> (options.<span class=property>models</span>) {</span><br><span class=line> <span class=keyword>const</span> <span class="title function_">noop</span> = (<span class=params></span>) => <span class=literal>undefined</span>;</span><br><span class=line> <span class=keyword>const</span> models = <span class="title function_">renderDeclarations</span>({</span><br><span class=line> <span class=attr>globals</span>: <span class="variable constant_">MODEL_GLOBAL_DECLARATIONS</span>.<span class="title function_">map</span>(<span class=function>(<span class=params><span class="variable language_">global</span></span>) =></span> ({ ...<span class="variable language_">global</span>, <span class=attr>execute</span>: noop })),</span><br><span class=line> });</span><br><span class=line> sections.<span class="title function_">push</span>(<span class=string>`Model API:\n\`\`\`ts\n<span class=subst>${MODEL_TYPES}</span>\n\n<span class=subst>${models}</span>\n\`\`\``</span>);</span><br><span class=line>}</span><br></pre></table></figure><p>改后删除了这段常驻注入,只在 <code>describeGlobals()</code> 中留下:<figure class="highlight ts"><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=keyword>if</span> (models) {</span><br><span class=line> lines.<span class="title function_">push</span>(<span class=string>`- \`models\`: classifiers and image generation. Read <span class=subst>${CODEMODE_DOCS_PATH}</span> first.`</span>);</span><br><span class=line>}</span><br></pre></table></figure><p><code>MODEL_TYPES</code> 本身有 54 行,此外还有 5 个方法的签名和说明。它们从每次请求中的 API 参考,变成需要时读取的文档。运行时的 API 仍然存在。<a href=https://github.com/earendil-works/pi/commit/6f1072cc081f06b86a673bd142f03720d17afe15 rel=noopener target=_blank>改动源码</a><h3 id=已声明工具:完整-TypeScript-声明改成调用与返回字段速览><a title="已声明工具:完整 TypeScript 声明改成调用与返回字段速览" class=headerlink href=#已声明工具:完整-TypeScript-声明改成调用与返回字段速览></a>已声明工具:完整 TypeScript 声明改成调用与返回字段速览</h3><p>默认 <code>codemode.mode = "on"</code> 时,<code>bash</code> 这样的直接工具已经有正常输入 schema。旧版又在工具 description 中追加一份完整 TypeScript 声明,构造代码是:<figure class="highlight ts"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>descriptions[tool.<span class=property>name</span>] = <span class="title function_">renderToolSample</span>(<span class="title function_">toCodemodeDeclaration</span>(tool));</span><br></pre></table></figure><p>以 <code>bash</code> 为例,下面是根据旧版 schema 和声明生成器还原的追加部分。共同保留的自然语言工具说明没有重复列出:<figure class="highlight ts"><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><span class=comment>// codemode tool declaration:</span></span><br><span class=line></span><br><span class=line><span class=keyword>declare</span> <span class=keyword>const</span> <span class=attr>tools</span>: {</span><br><span class=line> <span class="title function_">bash</span>(<span class=attr>args</span>: {</span><br><span class=line> <span class=comment>// Shell command to execute</span></span><br><span class=line> <span class=attr>command</span>: <span class=built_in>string</span>;</span><br><span class=line> <span class=comment>// Timeout in seconds (optional, no default timeout)</span></span><br><span class=line> timeout?: <span class=built_in>number</span>;</span><br><span class=line> }): <span class="title class_">Promise</span><{</span><br><span class=line> <span class=attr>exit_code</span>: <span class=built_in>number</span>;</span><br><span class=line> <span class=comment>// Temp file with the full output, when truncated</span></span><br><span class=line> full_output_path?: <span class=built_in>string</span>;</span><br><span class=line> <span class=comment>// Combined stdout and stderr, up to 1 MiB. Longer output keeps its first and last 512 KiB around an omission marker.</span></span><br><span class=line> <span class=attr>output</span>: <span class=built_in>string</span>;</span><br><span class=line> <span class=comment>// Whether `output` omits part of the command output</span></span><br><span class=line> <span class=attr>truncated</span>: <span class=built_in>boolean</span>;</span><br><span class=line> <span class=attr>wall_time_seconds</span>: <span class=built_in>number</span>;</span><br><span class=line> }>;</span><br><span class=line>};</span><br></pre></table></figure><p>1.0 改用 <code>describeScriptCall()</code>,输出类型通常只列字段名和 optional 标记:<figure class="highlight ts"><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=keyword>function</span> <span class="title function_">describeOutput</span>(<span class=params>schema: CodemodeJsonSchema | <span class=literal>undefined</span></span>): <span class=built_in>string</span> {</span><br><span class=line> <span class=keyword>const</span> <span class=keyword>type</span> = <span class="title function_">renderToolOutputType</span>(schema);</span><br><span class=line> <span class=keyword>if</span> (<span class=keyword>type</span> === <span class=string>"string"</span>) <span class=keyword>return</span> <span class=string>"a string"</span>;</span><br><span class=line> <span class=keyword>const</span> <span class=built_in>object</span> = <span class=keyword>typeof</span> schema === <span class=string>"object"</span> ? schema : <span class=literal>undefined</span>;</span><br><span class=line> <span class=keyword>const</span> properties = <span class=built_in>object</span>?.<span class=property>properties</span>;</span><br><span class=line> <span class=keyword>if</span> (</span><br><span class=line> <span class=built_in>object</span>?.<span class=property>type</span> === <span class=string>"object"</span> &&</span><br><span class=line> <span class=keyword>typeof</span> properties === <span class=string>"object"</span> &&</span><br><span class=line> properties !== <span class=literal>null</span> &&</span><br><span class=line> <span class="title function_">mcpStructuredContentSchema</span>(schema) === <span class=literal>undefined</span></span><br><span class=line> ) {</span><br><span class=line> <span class=keyword>const</span> required = <span class=keyword>new</span> <span class="title class_">Set</span>(<span class="title class_">Array</span>.<span class="title function_">isArray</span>(<span class=built_in>object</span>.<span class=property>required</span>) ? <span class=built_in>object</span>.<span class=property>required</span> : []);</span><br><span class=line> <span class=keyword>const</span> fields = <span class="title class_">Object</span>.<span class="title function_">keys</span>(properties).<span class="title function_">map</span>(<span class=function>(<span class=params>name</span>) =></span> (required.<span class="title function_">has</span>(name) ? name : <span class=string>`<span class=subst>${name}</span>?`</span>));</span><br><span class=line> <span class=keyword>return</span> <span class=string>`\`{ <span class=subst>${fields.join(<span class=string>", "</span>)}</span> }\``</span>;</span><br><span class=line> }</span><br><span class=line> <span class=keyword>return</span> <span class=string>`\`<span class=subst>${<span class=keyword>type</span>.replace(/\s+/g, <span class=string>" "</span>)}</span>\``</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=comment> * A declared tool's description followed by how scripts call it and what the call resolves to. The</span></span><br><span class=line><span class=comment> * arguments are the tool's declared parameters, so they are not repeated.</span></span><br><span class=line><span class=comment> */</span></span><br><span class=line><span class=keyword>function</span> <span class="title function_">describeScriptCall</span>(<span class=params>tool: AgentTool<<span class=built_in>any</span>></span>): <span class=built_in>string</span> {</span><br><span class=line> <span class=keyword>return</span> <span class=string>`<span class=subst>${tool.description.trim()}</span>\n\nCodemode: \`tools.<span class=subst>${toCodemodeIdentifier(tool.name)}</span>(args)\` resolves to <span class=subst>${describeOutput(toCodemodeDeclaration(tool).outputSchema)}</span>.`</span>;</span><br><span class=line>}</span><br></pre></table></figure><p>生成的 <code>bash</code> 追加说明变成:<figure class="highlight text"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line>Codemode: `tools.bash(args)` resolves to `{ output, truncated, full_output_path?, exit_code, wall_time_seconds }`.</span><br></pre></table></figure><p>模型已经能从正常工具 schema 读到 <code>command</code> 和 <code>timeout</code>,description 只需要补充脚本调用方式及返回值概况。这省掉了重复参数和冗长输出字段注释。<a href=https://github.com/earendil-works/pi/blob/88ff80b986e34d4fbd1fa94a4df65c60ae964516/packages/coding-agent/src/extensions/codemode/tool.ts#L405-L411 rel=noopener target=_blank>旧版声明构造</a>、<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/codemode/tool.ts#L289-L337 rel=noopener target=_blank>新版构造</a><p><code>codemode.mode = "only"</code> 的情况不同:模型只通过 Codemode 调用这些工具,正常直接声明被隐藏,Codemode description 会在预算内列出非 <code>deferred</code> 工具的完整嵌套声明。工具创建时默认采用下面的预算:<figure class="highlight ts"><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=keyword>export</span> <span class=keyword>const</span> <span class="variable constant_">DEFAULT_CODEMODE_INLINE_BUDGET</span> = <span class=number>3000</span>;</span><br><span class=line><span class=keyword>const</span> <span class="variable constant_">CHARS_PER_TOKEN</span> = <span class=number>4</span>;</span><br></pre></table></figure><p>这里的 3000 是按字符数除以 4 得到的估算 tokens。声明选择按 namespace 轮流进行,每组优先放成本较低的声明;没有 namespace 的工具也单独成组。超出预算的工具继续通过发现函数查询。因此,完整声明仍然保留在需要它的路径中,但不会无条件列出全部工具。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/codemode/tool.ts#L153-L226 rel=noopener target=_blank>预算与选择算法</a>、<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/codemode/tool.ts#L342-L354 rel=noopener target=_blank>创建工具时使用默认预算</a><h3 id=MCP-服务器段:固定解释所有路径改成按实际路径生成><a title="MCP 服务器段:固定解释所有路径改成按实际路径生成" class=headerlink href=#MCP-服务器段:固定解释所有路径改成按实际路径生成></a>MCP 服务器段:固定解释所有路径改成按实际路径生成</h3><p>旧版的服务器段开头始终说明 Codemode、工具搜索、namespace instructions 和 <code>tool_search</code>:<figure class="highlight ts"><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=keyword>const</span> <span class="variable constant_">SERVERS_SECTION_INTRO</span> =</span><br><span class=line> <span class=string>"MCP servers whose tools are not declared to you. Call the tools of `codemode` servers from codemode scripts: find them with `searchTools(query, { namespace })` and read a server's instructions and tool names with `describeNamespace(name)`. Load the tools of `tool_search` servers with `tool_search`."</span>;</span><br></pre></table></figure><p>改后根据当前服务器实际采用的路径拼接:<figure class="highlight ts"><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=keyword>function</span> <span class="title function_">serversSectionIntro</span>(<span class=params>reaches: ReadonlySet<<span class=built_in>string</span>></span>): <span class=built_in>string</span> {</span><br><span class=line> <span class=keyword>let</span> intro = <span class=string>"MCP servers whose tools are not declared to you."</span>;</span><br><span class=line> <span class=keyword>if</span> (reaches.<span class="title function_">has</span>(<span class=string>"codemode"</span>)) intro += <span class=string>" Call the tools of `codemode` servers from codemode scripts."</span>;</span><br><span class=line> <span class=keyword>if</span> (reaches.<span class="title function_">has</span>(<span class=string>"tool_search"</span>)) intro += <span class=string>" Load the tools of `tool_search` servers with `tool_search`."</span>;</span><br><span class=line> <span class=keyword>return</span> intro;</span><br><span class=line>}</span><br></pre></table></figure><p>固定 299 字符的说明,变成单一路径 108 字符、两种路径都有时 168 字符。工具发现方法已经在 globals 中出现,服务器段就不再重复详细用法。<a href=https://github.com/earendil-works/pi/blob/88ff80b986e34d4fbd1fa94a4df65c60ae964516/packages/coding-agent/src/extensions/mcp/index.ts#L158-L160 rel=noopener target=_blank>改前源码</a>、<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/mcp/index.ts#L158-L167 rel=noopener target=_blank>改后源码</a><h3 id=系统提示中的使用建议也缩短了><a class=headerlink href=#系统提示中的使用建议也缩短了 title=系统提示中的使用建议也缩短了></a>系统提示中的使用建议也缩短了</h3><p>改前:<figure class="highlight ts"><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=keyword>export</span> <span class=keyword>const</span> codemodeToolSystemPromptContribution = {</span><br><span class=line> <span class=attr>snippet</span>: <span class=string>"Run JavaScript that calls other tools (chains, loops, Promise.all, filtering large results)"</span>,</span><br><span class=line> <span class=attr>guidelines</span>: [</span><br><span class=line> <span class=string>"Use codemode to batch or chain several tool calls, or to filter large tool output down to what you need, instead of issuing many individual tool calls. Batch independent calls in one codemode call using await Promise.allSettled([...])."</span>,</span><br><span class=line> ],</span><br><span class=line>} <span class=keyword>as</span> <span class=keyword>const</span>;</span><br></pre></table></figure><p>改后:<figure class="highlight ts"><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=keyword>export</span> <span class=keyword>const</span> codemodeToolSystemPromptContribution = {</span><br><span class=line> <span class=attr>snippet</span>: <span class=string>"Run JavaScript that calls other tools"</span>,</span><br><span class=line> <span class=attr>guidelines</span>: [</span><br><span class=line> <span class=string>"Use codemode to batch independent tool calls (Promise.allSettled), chain them, or filter large output, instead of many separate calls."</span>,</span><br><span class=line> ],</span><br><span class=line>} <span class=keyword>as</span> <span class=keyword>const</span>;</span><br></pre></table></figure><p>摘要从 91 字符缩到 37 字符,使用建议从 235 字符缩到 134 字符。独立调用用 <code>Promise.allSettled()</code>、串联调用、筛选大结果这几项建议都保留了,只减少重复解释。<p>这些修改共同解释了官方 5300 → 3300 prompt tokens 的样本。后续如果需要读取文档,文档内容仍会进入相应请求;真实任务的收益取决于工具规模、调用方式和查询频率。<h2 id=描述保持稳定,服务器在后台连接><a class=headerlink href=#描述保持稳定,服务器在后台连接 title=描述保持稳定,服务器在后台连接></a>描述保持稳定,服务器在后台连接</h2><p>0.99.2 已经把默认 MCP 工具从 Codemode description 中移出。服务器连接完成或工具列表变化时,不需要重写整份 Codemode 描述,也不再把工具数量和完整 instructions 塞进去。1.0 延续了这个结构。<p>MCP 服务器改为列在 <code>mcp_servers</code> 系统提示段中。<code>renderServersSection()</code> 使用一行摘要,单个摘要最多 250 字符,整个段最多 4096 字符;完整 instructions 通过 <code>describeNamespace()</code> 查询。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/mcp/index.ts#L149-L213 rel=noopener target=_blank>服务器段源码</a><p>服务器段变化时,产品层计算系统提示 section 的差异,把更新追加到会话。稳定前缀有利于缓存复用,但本文没有测量各家 provider 的实际缓存命中率。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/core/agent-session.ts#L1689-L1703 rel=noopener target=_blank>系统提示差异构造</a><p>后台连接也有明确的等待规则:会话启动时,Pi 开始连接所有启用的服务器;首次 prompt 只等待配置了 <code>direct</code> 工具的服务器,默认最多 10 秒。其余连接在工具需要时等待。<p>脚本需要哪些服务器,当前通过下面的代码判断:<figure class="highlight ts"><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>function</span> <span class="title function_">scriptNeedsServer</span>(<span class=params>code: <span class=built_in>string</span>, server: <span class=built_in>string</span></span>): <span class=built_in>boolean</span> {</span><br><span class=line> <span class=keyword>if</span> (<span class=regexp>/\b(searchTools|describeNamespace|describeTool|ALL_TOOLS)\b/</span>.<span class="title function_">test</span>(code)) <span class=keyword>return</span> <span class=literal>true</span>;</span><br><span class=line> <span class=keyword>return</span> code.<span class="title function_">includes</span>(<span class="title function_">mcpNamespace</span>(server));</span><br><span class=line>}</span><br></pre></table></figure><p>直接写出某个 <code>mcp__&LTserver></code> 命名空间,会等待对应服务器。代码中出现 <code>searchTools</code>、<code>describeNamespace</code>、<code>describeTool</code> 或 <code>ALL_TOOLS</code> 时,则等待全部尚在连接的启用服务器。即使给 <code>searchTools()</code> 指定 namespace,这个前置等待也依旧覆盖全部服务器。<p>这里使用正则和字符串匹配,没有分析 JavaScript 语法树。代码注释中出现这些名字也可能命中。因此,首次请求可以很快发出去,首次搜索工具仍可能承担服务器连接等待。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/mcp/index.ts#L954-L1045 rel=noopener target=_blank>启动与等待实现</a><p>工具调用时的 <code>waitForServers()</code> 会等到对应服务器的 <code>ready</code> promise 完成或 AbortSignal 取消;这次按需等待不受首次 prompt 的 10 秒上限约束。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/mcp/index.ts#L548-L560 rel=noopener target=_blank>按需等待实现</a><h2 id=1-0-的纠错提示让模型有明确的恢复路径><a title="1.0 的纠错提示让模型有明确的恢复路径" class=headerlink href=#1-0-的纠错提示让模型有明确的恢复路径></a>1.0 的纠错提示让模型有明确的恢复路径</h2><p>说明变短以后,模型可能写错工具名。1.0 为 <code>tools</code> 和 <code>models</code> 等对象加了 Proxy guard:读取不存在的成员时,立即抛出错误,并给出相近名称或可用成员列表。例如 <code>tools.Bash</code> 会提示 <code>tools.bash</code>。<p>这样做也改变了 JavaScript 探测行为。下面的旧式写法可能直接抛错:<figure class="highlight js"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=keyword>typeof</span> tools.<span class=property>some_missing_tool</span> === <span class=string>"function"</span></span><br></pre></table></figure><p>需要改成成员存在性检查:<figure class="highlight js"><table><tr><td class=gutter><pre><span class=line>1</span><br></pre><td class=code><pre><span class=line><span class=string>"some_missing_tool"</span> <span class=keyword>in</span> tools</span><br></pre></table></figure><p>相近名称匹配使用大小写和标点归一化,以及字符串包含关系。它给模型一个修正入口;工具搜索和完整文档仍是另一条恢复路径。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/codemode/src/runtime/prelude-source.ts#L116-L159 rel=noopener target=_blank>Proxy guard 源码</a><h2 id=OAuth:少一些反复登录,账号和权限也要存对><a class=headerlink href=#OAuth:少一些反复登录,账号和权限也要存对 title=OAuth:少一些反复登录,账号和权限也要存对></a>OAuth:少一些反复登录,账号和权限也要存对</h2><p>远程 MCP 接上以后,常见的麻烦是登录成功了,换一个操作又提示权限不足;或者工作账号和个人账号指向同一个服务,后登录的账号覆盖了前一个。1.0 的 OAuth 改动主要解决这些状态问题。<h3 id=补权限时保留已经拿到的权限><a class=headerlink href=#补权限时保留已经拿到的权限 title=补权限时保留已经拿到的权限></a>补权限时保留已经拿到的权限</h3><p>假设 token 已有 <code>issues:read</code>,服务端返回 <code>insufficient_scope</code>,要求补上 <code>issues:write</code>。这个错误表示当前授权不够用。0.99.2 的适配器只把 challenge 中的 scope 交给下一轮授权,新的 token 可能只剩写权限,下一次读操作又要求登录。<p>1.0 的 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/src/oauth/flow.ts#L270-L279 rel=noopener target=_blank><code>stepUpScope()</code></a> 合并已有权限和本次要求,再去重。下面是源码原文:<figure class="highlight ts"><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>function</span> <span class="title function_">stepUpScope</span>(<span class=params>granted: <span class=built_in>string</span> | <span class=literal>undefined</span>, challenged: <span class=built_in>string</span> | <span class=literal>undefined</span></span>): <span class=built_in>string</span> | <span class=literal>undefined</span> {</span><br><span class=line> <span class=keyword>if</span> (!challenged) <span class=keyword>return</span> <span class=literal>undefined</span>;</span><br><span class=line> <span class=keyword>const</span> scopes = [granted, challenged].<span class="title function_">flatMap</span>(<span class=function>(<span class=params>scope</span>) =></span> scope?.<span class="title function_">split</span>(<span class=regexp>/\s+/</span>).<span class="title function_">filter</span>(<span class="title class_">Boolean</span>) ?? []);</span><br><span class=line> <span class=keyword>return</span> [...<span class=keyword>new</span> <span class="title class_">Set</span>(scopes)].<span class="title function_">join</span>(<span class=string>" "</span>);</span><br><span class=line>}</span><br></pre></table></figure><p>记录旧权限也得跟上。token 响应省略 <code>scope</code> 时,流程保存本次请求的 scope;refresh 响应省略它时,保存旧 token 的 scope。这分别沿用 OAuth 对授权响应和刷新的规则,见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/src/oauth/flow.ts#L339-L364 rel=noopener target=_blank><code>withScope()</code> 的调用</a>。权限提升会跳过 refresh,重新走浏览器授权,因为刷新原来的 grant 无法补出新权限。<p>仓库的<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/test/mcp-oauth-refresh.test.ts#L115-L152 rel=noopener target=_blank>登录测试</a> 明确断言,授权 URL 的 scope 从 <code>issues:read</code> 变成 <code>issues:read issues:write</code>。旧记录如果没有 scope,代码无法还原过去实际授予的权限;challenge 没给 scope 时,函数也会返回 <code>undefined</code>,交给流程选择默认来源。<h3 id=同一个服务地址可以保留多个账号><a class=headerlink href=#同一个服务地址可以保留多个账号 title=同一个服务地址可以保留多个账号></a>同一个服务地址可以保留多个账号</h3><p><a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/mcp/oauth.ts#L116-L158 rel=noopener target=_blank><code>McpOAuthCredentialStore</code></a> 把 <code>mcp-auth.json</code> 的键从 URL 改成 server name 加 URL:<figure class="highlight ts"><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>function</span> <span class="title function_">storeKeys</span>(<span class=params>name: <span class=built_in>string</span>, serverUrl: <span class=built_in>string</span></span>): { <span class=attr>key</span>: <span class=built_in>string</span>; <span class=attr>legacyKey</span>: <span class=built_in>string</span> } {</span><br><span class=line> <span class=keyword>const</span> legacyKey = <span class="title class_">String</span>(<span class=keyword>new</span> <span class="title function_">URL</span>(serverUrl));</span><br><span class=line> <span class=keyword>return</span> { <span class=attr>key</span>: <span class=string>`<span class=subst>${mcpNamespace(name)}</span>|<span class=subst>${legacyKey}</span>`</span>, legacyKey };</span><br><span class=line>}</span><br></pre></table></figure><p>于是 <code>work</code> 和 <code>personal</code> 即使连接相同 URL,也有独立的 token、client registration 和刷新锁。升级时,URL-only 旧记录归第一个加载它的 server,迁移在存储锁内完成,随后删除旧键;其他同 URL 配置需要重新登录。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/test/mcp-oauth-store.test.ts#L15-L54 rel=noopener target=_blank>测试</a> 还说明,<code>my-work</code> 和 <code>my_work</code> 经 namespace 规范化后算同一个名称。由键的结构可以推断,改名也可能让配置失去原来的凭据关联。<h3 id=检查授权回调来自哪个-issuer><a title="检查授权回调来自哪个 issuer" class=headerlink href=#检查授权回调来自哪个-issuer></a>检查授权回调来自哪个 issuer</h3><p>1.0 在交换 authorization code 之前加入 RFC 9207 的 <code>iss</code> 检查。<code>iss</code> 是回调携带的授权服务器身份,防止把一个服务器签发的 code 送给另一个服务器。浏览器回调和手动粘贴 redirect URL 都会传递它。核心判断见 <a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/src/oauth/flow.ts#L339-L350 rel=noopener target=_blank><code>runFlow()</code></a>:<figure class="highlight ts"><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=keyword>if</span> (metadata && (iss !== <span class=literal>undefined</span> || metadata.<span class=property>authorization_response_iss_parameter_supported</span>)) {</span><br><span class=line> <span class=keyword>if</span> (iss !== metadata.<span class=property>issuer</span>) <span class=keyword>throw</span> <span class=keyword>new</span> <span class="title class_">OAuthIssuerMismatchError</span>(metadata.<span class=property>issuer</span>, iss);</span><br><span class=line>}</span><br></pre></table></figure><p>提供了 <code>iss</code>,就必须严格等于 metadata 中的 issuer;服务器声明支持 <code>iss</code> 却没有返回,也拒绝交换。没有声明支持、回调也没有它时,保持兼容。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/test/oauth.test.ts#L469-L500 rel=noopener target=_blank>测试</a> 检查了这些组合,并断言错误 code 没有发到 token endpoint。这个判断以存在 metadata 为前提,旧式 fallback 没有 metadata 时,不能据此认定回调 issuer 得到了验证。<h3 id=Discovery-有问题时可以指定-metadata><a title="Discovery 有问题时可以指定 metadata" class=headerlink href=#Discovery-有问题时可以指定-metadata></a>Discovery 有问题时可以指定 metadata</h3><p><code>oauth.authServerMetadataUrl</code> 让配置直接指向授权服务器的 metadata 文档,适合 MCP 服务报错授权地址或没有可用发现信息的情况。它下传为 <code>authorizeMcp()</code> 的 <code>authorizationServerMetadataUrl</code>。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/src/oauth/discovery.ts#L126-L164 rel=noopener target=_blank>实现</a> 信任这个配置,使用文档中的 issuer 和端点,不把 issuer 与文档 URL 做自动发现时的匹配校验。回调 <code>iss</code> 仍与这个 issuer 比较。<p><a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/src/oauth/flow.ts#L281-L307 rel=noopener target=_blank><code>runFlow()</code></a> 要求该 URL 使用 HTTPS 或 loopback,并在配置覆盖时停用 discovery 缓存,确保改 URL 后立即生效。代价是每次流程都要重新请求 metadata。指定文档返回 404 也会直接报错。这项能力把授权服务器的选择交给配置维护者,不能理解成自动跳过所有认证检查。<h3 id=空字段不再破坏整条登录流程><a class=headerlink href=#空字段不再破坏整条登录流程 title=空字段不再破坏整条登录流程></a>空字段不再破坏整条登录流程</h3><p>有些服务端会返回 <code>scope: ""</code>、<code>refresh_token: ""</code> 或 <code>expires_in: null</code>。1.0 的<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/src/oauth/types.ts#L105-L132 rel=noopener target=_blank>解析器</a> 将可选字段的空串和 <code>null</code> 按未提供处理,必需的 <code>access_token</code> 等字段仍要校验。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/mcp/src/oauth/types.ts#L178-L190 rel=noopener target=_blank>过期时间解析</a> 也避免了 <code>Number(null) === 0</code> 导致刚拿到 token 就被当作过期。<p>这些修正与恢复登录状态有关,但并发 401 共用一次 refresh、避免轮换 refresh token 被重复使用的机制,在 0.99.2 已存在。1.0 补上的是 scope 记录、账号隔离和输入兼容性。本文对照了源码及测试断言,没有执行测试,也没有据此计算登录成功率或延迟改善。<h2 id=恢复会话时,先保存还没有注册的工具名><a class=headerlink href=#恢复会话时,先保存还没有注册的工具名 title=恢复会话时,先保存还没有注册的工具名></a>恢复会话时,先保存还没有注册的工具名</h2><p><code>tool_search</code> 加载的工具会记录在 transcript,也就是会话记录中。恢复会话或执行 <code>/reload</code> 时,读取工具列表的时间可能早于 MCP 服务器重连完成。工具尚未注册,就会被旧的 active tool 构造过程过滤掉。<p>1.0 增加 <code>_pendingToolNames</code>,让恢复的工具名先进入待处理集合:<figure class="highlight ts"><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><span class=keyword>private</span> <span class="title function_">_restoreToolsFromTranscript</span>(): <span class=built_in>void</span> {</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_pendingToolNames</span>.<span class="title function_">clear</span>();</span><br><span class=line> <span class=keyword>const</span> current = <span class="title function_">getCurrentSystemMessage</span>(<span class="variable language_">this</span>.<span class=property>sessionManager</span>.<span class="title function_">buildSessionContext</span>().<span class=property>messages</span>);</span><br><span class=line> <span class=keyword>if</span> (!current) <span class=keyword>return</span>;</span><br><span class=line> <span class=keyword>const</span> names = (current.<span class=property>toolsAdded</span> ?? []).<span class="title function_">map</span>(<span class=function>(<span class=params>tool</span>) =></span> tool.<span class=property>name</span>);</span><br><span class=line> <span class="variable language_">this</span>.<span class=property>_pendingToolNames</span> = <span class=keyword>new</span> <span class="title class_">Set</span>(names.<span class="title function_">filter</span>(<span class=function>(<span class=params>name</span>) =></span> <span class="variable language_">this</span>.<span class="title function_">_isAllowedTool</span>(name)));</span><br><span class=line> <span class="variable language_">this</span>.<span class="title function_">_setActiveTools</span>(names);</span><br><span class=line> }</span><br></pre></table></figure><p>注册工具、重建运行时时,再把这些名字加入下一组 active tools:<figure class="highlight ts"><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=comment>// Pending tools that are registered now become active.</span></span><br><span class=line>nextActiveToolNames.<span class="title function_">push</span>(...<span class="variable language_">this</span>.<span class=property>_pendingToolNames</span>);</span><br><span class=line></span><br><span class=line><span class="variable language_">this</span>.<span class="title function_">_setActiveTools</span>([...<span class=keyword>new</span> <span class="title class_">Set</span>(nextActiveToolNames)]);</span><br></pre></table></figure><p>这个集合有生命周期:工具注册完成后会从 pending 删除;扩展主动移除 active tool 时,会清掉原恢复集合;下一次 agent run 开始时,仍未注册的名字也会清掉。它修复了及时重连时的工具丢失,极慢连接仍可能需要重新发现。<p>仓库测试分别覆盖了 resume、<code>/reload</code>、扩展修改 loadout,以及服务器在下一次 prompt 之后才连接的情形。<a href=https://github.com/earendil-works/pi/commit/c662ec7e374563bd549dc35f47bac52dcc4bed88 rel=noopener target=_blank>修复 commit</a>、<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/test/suite/agent-session-mcp.test.ts#L1054-L1117 rel=noopener target=_blank>测试断言</a><h2 id=当前实现还有哪些边界><a class=headerlink href=#当前实现还有哪些边界 title=当前实现还有哪些边界></a>当前实现还有哪些边界</h2><p>工具搜索的分词逻辑值得直接看一眼:<figure class="highlight ts"><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></pre><td class=code><pre><span class=line><span class=keyword>export</span> <span class=keyword>function</span> <span class="title function_">tokenize</span>(<span class=params>text: <span class=built_in>string</span></span>): <span class=built_in>string</span>[] {</span><br><span class=line> <span class=keyword>return</span> text</span><br><span class=line> .<span class="title function_">replace</span>(<span class=regexp>/([a-z0-9])([A-Z])/g</span>, <span class=string>"$1 $2"</span>)</span><br><span class=line> .<span class="title function_">replace</span>(<span class=regexp>/([A-Z]+)([A-Z][a-z])/g</span>, <span class=string>"$1 $2"</span>)</span><br><span class=line> .<span class="title function_">toLowerCase</span>()</span><br><span class=line> .<span class="title function_">split</span>(<span class=regexp>/[^a-z0-9]+/</span>)</span><br><span class=line> .<span class="title function_">filter</span>(<span class=function>(<span class=params>term</span>) =></span> term.<span class=property>length</span> > <span class=number>0</span> && !<span class="variable constant_">STOP_WORDS</span>.<span class="title function_">has</span>(term))</span><br><span class=line> .<span class="title function_">map</span>(stem);</span><br><span class=line>}</span><br></pre></table></figure><p><code>split(/[^a-z0-9]+/)</code> 只保留 ASCII 字母和数字。实际传给 <code>searchTools()</code> 或 <code>tool_search</code> 的纯中文 query 会没有词项,BM25 随即返回空匹配。中文对话仍可以由模型转换成英文关键词、使用工具标识符,或改走 <code>describeNamespace()</code>、<code>describeTool()</code> 和 <code>ALL_TOOLS</code>。当前这两条检索路径内没有自动翻译或 embedding fallback。<a href=https://github.com/earendil-works/pi/blob/a13d35a742c6ef8462812a28fbe1d8c8b7431c32/packages/coding-agent/src/extensions/tool-search/tool.ts#L72-L155 rel=noopener target=_blank>分词与排序源码</a><p>传输层支持 stdio 和 Streamable HTTP。stdio 是启动本地进程,通过标准输入输出交换协议消息;Streamable HTTP 用于远程服务器。1.0.0 不支持旧式 SSE transport,也不渲染 MCP Apps 的 <code>ui://</code> 资源。<a href=https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/mcp.md#L46-L59 rel=noopener target=_blank>MCP 文档</a>、<a href=https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/mcp.md#L176-L185 rel=noopener target=_blank>资源边界</a><p>QuickJS 隔离的是脚本运行环境。脚本仍能通过注入工具产生真实外部操作,脚本失败也不会撤销已完成的调用。参数校验和权限扩展继续留在正常工具流水线上。<a href=https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/docs/codemode.md#L3-L14 rel=noopener target=_blank>脚本执行说明</a><p>1.0 加入官方 MCP conformance suite 的 CI,但它按已提交的测试基线检查回归。覆盖的协议版本为 <code>2025-03-26</code>、<code>2025-06-18</code> 和 <code>2025-11-25</code>;1.0.0 仍有 elicitation、CIMD 等已知缺口,也没有实现 <code>2026-07-28</code> 的 stateless 协议。后续版本文档里的新增能力,需要单独核对 tag。<a href=https://github.com/earendil-works/pi/blob/v1.0.0/packages/coding-agent/test/mcp-conformance/README.md rel=noopener target=_blank>测试范围说明</a><h2 id=按使用方式选择工具曝光><a class=headerlink href=#按使用方式选择工具曝光 title=按使用方式选择工具曝光></a>按使用方式选择工具曝光</h2><p>接入时可以按工具使用方式配置 exposure。下面是一个示例,URL 是占位地址,工具名和通配符需要按实际服务器调整:<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></pre><td class=code><pre><span class=line><span class=punctuation>{</span></span><br><span class=line> <span class=attr>"mcpServers"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"tracker"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"url"</span><span class=punctuation>:</span> <span class=string>"https://example.com/mcp"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"description"</span><span class=punctuation>:</span> <span class=string>"Search and read issue tracker data"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"exposure"</span><span class=punctuation>:</span> <span class=string>"codemode"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"toolExposure"</span><span class=punctuation>:</span> <span class=punctuation>{</span></span><br><span class=line> <span class=attr>"search_issues"</span><span class=punctuation>:</span> <span class=string>"direct"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"list_*"</span><span class=punctuation>:</span> <span class=string>"codemode"</span><span class=punctuation>,</span></span><br><span class=line> <span class=attr>"delete_*"</span><span class=punctuation>:</span> <span class=string>"hidden"</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>少量、经常直接调用的工具可以提前声明;适合批量处理和筛选结果的工具可以留给 Codemode;希望发现后再直接调用的工具可以使用 <code>deferred</code>。<code>hidden</code> 控制工具在当前 Agent 中是否可达,服务器端的账号授权仍需独立配置。<p>阅读这套实现时,可以沿着 <code>mcp/index.ts</code> 看注册与等待,沿着 <code>codemode/tool.ts</code> 看模型能读到的说明,再沿着 <code>codemode/execute.ts</code> 和 <code>mcp/tools.ts</code> 看调用与结果转换。这样能把服务器连接、模型上下文和脚本执行三个过程对应起来。</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 1.0 的 MCP 集成源码解析:工具发现、代码编排与提示词压缩" href=https://nicksxs.me/2026/10/04/Pi-Agent-1-0-MCP%E9%9B%86%E6%88%90%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90-%E5%B7%A5%E5%85%B7%E5%8F%91%E7%8E%B0%E4%B8%8E%E6%8F%90%E7%A4%BA%E8%AF%8D%E5%8E%8B%E7%BC%A9/>https://nicksxs.me/2026/10/04/Pi-Agent-1-0-MCP集成源码解析-工具发现与提示词压缩/</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/MCP/ rel=tag># MCP</a><a href=/tags/TypeScript/ rel=tag># TypeScript</a></div><div class=post-nav><div class=post-nav-item><a title="Qwen-Image 2.1 入门:从一句提示词到一张图,6GB 显存为什么也能跑" href=/2026/09/27/Qwen-Image-2-1%E5%85%A5%E9%97%A8-%E4%B8%BA%E4%BB%80%E4%B9%88%E5%AE%83%E5%80%BC%E5%BE%97%E5%85%B3%E6%B3%A8%E4%BB%A5%E5%8F%8A%E4%BD%8E%E9%85%8D%E6%98%BE%E5%8D%A1%E5%A6%82%E4%BD%95%E8%B7%91%E8%B5%B7%E6%9D%A5/ rel=prev><i class="fa fa-angle-left"></i> Qwen-Image 2.1 入门:从一句提示词到一张图,6GB 显存为什么也能跑</a></div><div class=post-nav-item><a title="腾讯 Octop 是什么:WorkBuddy 对比、Agent Loop 与本地部署" href=/2026/10/10/octop-local-deployment/ rel=next>腾讯 Octop 是什么:WorkBuddy 对比、Agent Loop 与本地部署 <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>