我如何将编程 Agent 的上下文开销削减 80% 以上
英文标题:🔶 I Cut 80%+ of Context Overhead in My Coding Agent
作者指出,现代 AI 编程 Agent 在用户发送第一条消息前,常因系统提示、工具 schema、MCP 集成等静态内容消耗 1 万至 2.5 万以上 token。其测试显示,Codex 在仅发送“hi”后已消耗 14,534 token 并加载 79 个工具;Antigravity 虽只有 17 个工具,但工具 schema 单独占用 13.8k token,总开销达 19.9k token。作者认为,长期加载大量低频工具不仅增加成本和延迟,也会稀释模型注意力、提高工具选择和参数调用出错的概率。为此,作者提出以 action 参数合并 CRUD 工具,并在 Pi 中仅常驻 4 个基础工具、按需动态激活其余工具,以降低上下文负担。
51.3 KB
82.2 KB
69.6 KB
51.8 KB
中文
当你在现代 AI 编程 Agent 中启动一个会话时,在输入第一条消息之前,上下文窗口中就已经被消耗了很大一部分。
系统指令、格式规则、MCP 服务器集成以及数十个已注册的工具 schema,共同导致大多数 Agent 框架会在每一轮将 10,000 至 25,000+ token 的静态开销塞入上下文窗口。
在 90% 的轮次中,Agent 只需要基础的文件和 Shell 工具(read、bash、edit、write)。浏览器自动化、图像生成、网页搜索或后台任务运行器等专用工具只是偶尔需要,有时甚至一周只用一次。
若始终让 25 至 80+ 个工具定义处于 LLM 函数 schema 中,会浪费 token、增加延迟,并因无关参数污染注意力空间而削弱模型的推理能力。
我通过两项设计决策解决了这个问题:
- 基于 action 的工具整合。 从第一天起就构建自定义工具,以避免 CRUD schema 的重复。
- 在 Pi 中动态激活工具。 只保持 4 个工具作为常驻基线,其余工具全部待命;让模型或用户按需激活工具,不产生元工具 schema 开销,并自动通过 TTL 清理。
对不同 Agent 框架的第零轮上下文进行基准测试
为了衡量问题的规模,我测试了不同编程 Agent 框架在全新会话中如何处理工具 schema 和上下文:测试方法是只发送一句问候语:"hi"。
1. Codex:默认 79 个工具和 14.5k token
我关闭了 Codex 中所有外部插件和 MCP 服务器,只保留两个自定义技能以及默认内置配置。随后,我启动了一个全新会话并发送 "hi"。
模型以标准的一行问候语作答(“Hi! How can I help?”)。线程状态显示,该会话已消耗 14,534 token(258k 上下文窗口的 6% 在第零轮就已消失)。
图 1:Codex 在发送单个“hi”后的上下文消耗。在任何实际工作开始前,已消耗 14,534 token。
当我询问 Agent 当前有哪些活跃且可调用的工具时,它返回了 79 个活跃工具。
点击查看 Codex 中加载的全部 79 个活跃工具
apply_patch
codex_app__automation_update
codex_app__create_thread
codex_app__fork_thread
codex_app__get_handoff_status
codex_app__handoff_thread
codex_app__list_archived_threads
codex_app__list_projects
codex_app__list_threads
codex_app__load_workspace_dependencies
codex_app__navigate_to_codex_page
codex_app__open_in_codex
codex_app__read_thread
codex_app__read_thread_terminal
codex_app__send_message_to_thread
codex_app__set_thread_archived
codex_app__set_thread_pinned
codex_app__set_thread_title
codex_app__share_thread
codex_app__wait_threads
create_goal
exec_command
get_goal
image_gen__imagegen
list_available_plugins_to_install
list_mcp_resource_templates
list_mcp_resources
mcp__codex_apps__codex_document_control_execute_document_command
mcp__codex_apps__codex_document_control_get_document_tool_schemas
mcp__codex_apps__codex_document_control_list_document_sessions
mcp__codex_apps__plugin_management_get_app_permissions
mcp__codex_apps__plugin_management_get_plugin_dependencies
mcp__codex_apps__plugin_management_uninstall_app
mcp__codex_apps__plugin_management_update_app_permissions
mcp__codex_apps__safety_settings_get_family_info
mcp__codex_apps__safety_settings_get_parental_controls
mcp__codex_apps__safety_settings_get_trusted_contact
mcp__codex_apps__safety_settings_prepare_parental_control_update
mcp__codex_apps__safety_settings_update_parental_control
mcp__codex_apps__sites_add_custom_domain
mcp__codex_apps__sites_change_site_slug
mcp__codex_apps__sites_create_site
mcp__codex_apps__sites_create_source_repository_write_credential
mcp__codex_apps__sites_deploy_private_site_version
mcp__codex_apps__sites_deploy_site_version
mcp__codex_apps__sites_generate_siwc_bypass_token
mcp__codex_apps__sites_get_deployment_status
mcp__codex_apps__sites_get_environment_variables
mcp__codex_apps__sites_get_site
mcp__codex_apps__sites_get_site_version
mcp__codex_apps__sites_get_site_worker_logs
mcp__codex_apps__sites_list_custom_domains
mcp__codex_apps__sites_list_site_versions
mcp__codex_apps__sites_list_sites
mcp__codex_apps__sites_read_database_overview
mcp__codex_apps__sites_read_database_table_rows
mcp__codex_apps__sites_refresh_custom_domain_status
mcp__codex_apps__sites_remove_custom_domain
mcp__codex_apps__sites_save_site_version
mcp__codex_apps__sites_update_environment_variables
mcp__codex_apps__sites_update_site_access
mcp__codex_apps__sites_update_site_metadata
mcp__node_repl__js
mcp__node_repl__js_add_node_module_dir
mcp__node_repl__js_reset
multi_agent_v1__close_agent
multi_agent_v1__resume_agent
multi_agent_v1__send_input
multi_agent_v1__spawn_agent
multi_agent_v1__wait_agent
plugin_management__uninstall_plugin
read_mcp_resource
request_permissions
request_plugin_install
update_goal
update_plan
view_image
web__run
write_stdin
如果只启用一两个额外插件,例如安全扫描器或 GPT 应用,活跃工具列表就会超过 100 个可调用工具。
2. Gemini 和 Antigravity:工具更少,但仍消耗 19.9k token
你可能会认为,缩短工具列表就能避免上下文膨胀。搭载 Gemini 3.7 Flash 的 Antigravity CLI(agy)表明,工具数量并非全部问题所在。
我进行了完全相同的测试:打开一个全新会话,并发送 "hi"。
回复只有一行(“Hello! How can I help you with your project today?”)。遥测数据表明,在第零轮立即消耗了 19.9k token,其中仅工具 schema 就占据了 13.8k token。
图 2:Antigravity CLI 在发送单个“hi”后的上下文构成。其中仅工具 schema 就消耗了 13.8k token。
Antigravity 只有 17 个工具处于活跃状态,而不是 79 个。然而,仅其工具 schema 就消耗了 13.8k token。
点击查看 Antigravity 中的 17 个活跃工具
1. run_command
2. manage_task
3. schedule
4. define_subagent
5. invoke_subagent
6. manage_subagents
7. send_message
8. write_to_file
9. replace_file_content
10. view_file
11. list_dir
12. grep_search
13. find_by_name
14. search_web
15. read_url_content
16. generate_image
17. ask_question
冗余问题
既然 Agent 已拥有 run_command(原生 bash),为什么还要为 list_dir、grep_search 和 find_by_name 创建专用 LLM 工具?
拥有 bash 访问能力的 Agent 可以直接运行 ls、grep、rg 或 find。为基础 Shell 操作额外构建独立的函数 schema,会重复模型已经具备的能力,同时在每一轮增加数千 token 的 JSON schema 定义、参数文档和边界情况说明。
3. Claude Code:内置基线与 MCP 困境
Anthropic 在 Claude Code 中意识到了这一问题,并引入了 Deferred Tool Loading(延迟工具加载)。
在未连接任何 MCP 服务器的开箱即用状态下,Claude Code 会维持约 8 至 10 个内置工具的基线(Bash、View/Read、Edit、Replace、Glob、Grep、Agent、WebSearch、NotebookEdit)。加上系统提示和项目指令,第零轮消耗约为 3,500 至 5,000+ token。
当开发者为数据库、问题追踪器或浏览器自动化连接多个 MCP 服务器时,工具注册表会超过 30 至 40+ 个工具,仅工具 schema 的每轮负载就会升至 10,000 至 14,000+ token。
当可延迟定义超过上下文窗口的 10% 时,Claude Code 会将工具分为两层:
-
始终加载(Always Loaded): 核心文件和搜索工具,以及基础设施工具(
Bash、Read、Edit、Write、Glob、Grep、Agent、ToolSearch、Skill)。 -
延迟加载(仅名称):
WebSearch、NotebookEdit、cron 自动化工具,以及所有已连接的 MCP 扩展工具。Always Loaded: Bash, Read, Edit, Write, Glob, Grep, Agent, ToolSearch, Skill
Deferred (Names only until fetched): WebSearch, TodoWrite, NotebookEdit, CronCreate, MCP servers...
尽管在启用大量 MCP 时,延迟加载能减少第零轮膨胀,但其发现机制依赖一个 LLM 元工具(ToolSearch):
- 当模型需要一个延迟工具时,必须先执行
ToolSearch("select:ToolName")。 - 后端会将完整 schema 注入上下文,模型只能在下一轮执行该工具。
- 为防止模型在上下文压缩期间忘记已加载的工具,运行时需要维护自定义边界元数据和压缩恢复逻辑。
- 即使处于最精简状态,Claude Code 仍会永久加载 9 个工具(包括冗余搜索工具及
ToolSearch元工具本身),维持数千 token 的基线开销。
关于 Claude Code 数据的说明:由于我个人并不使用 Claude Code,这些数据基于其他工程师分享的技术分析、遥测信息以及网上的社区讨论。我采用了活跃用户在标准配置下报告的下限估计值。
为什么第零轮开销会降低 Agent 性能
现代框架中的这种设计模式会带来两个问题:
- Token 成本和上下文耗尽。 在第零轮烧掉 14k 至 20k token,意味着你会更快触及上下文限制和速率配额。在包含长推理链的多轮会话中,每一次请求都要重新发送那 17 至 80+ 个工具 schema。
- 注意力稀释与工具混淆。 模型在决策空间更聚焦时表现最佳。当 LLM 面对数十个相似工具时——例如多个线程管理端点、站点部署工具、多 Agent 启动器、重复的搜索工具——它会将推理能力耗费在筛选无关选项上,也更容易产生参数幻觉或选错工具。
原则一:以 action 整合取代 CRUD API
削减上下文膨胀不能从运行时技巧开始,而要从第一天设计单个工具的方式开始。
在传统软件工程中,REST 和 CRUD 原则鼓励为每个操作创建细粒度端点:
memory_readmemory_writememory_updatememory_delete
这对于 HTTP API 来说是合理的,因为在客户端发起请求前,在代码中额外注册一个端点没有运行时负载成本。
但对于 AI Agent,这一假设完全不成立。每个工具 schema 都会在每一轮通过网络发送,并载入 LLM 的上下文窗口。 四个独立的 CRUD 工具意味着四组 JSON 头、四段描述、四个参数对象,以及四个挤占模型决策空间的条目。
基于 action 的工具整合
当我为 Agent 设计自定义工具时,我会按意图整合操作。对于记忆功能,我最多将接口拆分成两个工具:
-
memory_read:处理语义搜索、关键词查找和获取特定记忆。 -
memory_write:通过传入action参数("create" | "update" | "delete"),处理存储新记忆、更新已有条目和删除记忆。{ "name": "memory_write", "description": "Create, update, or delete entries in agent memory.", "parameters": { "type": "object", "properties": { "action": { "type": "string", "enum": ["create", "update", "delete"], "description": "The mutation action to perform." }, "id": { "type": "string", "description": "Memory ID (required for update or delete)." }, "content": { "type": "string", "description": "Memory content (required for create or update)." } }, "required": ["action"] } }
memory_write 的 schema 仅比单一 memory_create schema 大约大 15% 至 20%,但它以一个工具定义替换了三个独立定义。在不损失任何功能的情况下,你可以将 schema 占用缩减 50%。
如果希望在 token 效率上更激进,我会将所有记忆交互压缩为一个 memory 工具,并使用 action 枚举(search、fetch、create、update、delete)。
现代 LLM 能够可靠地处理 action 参数化工具。我已在多样化任务中连续数月使用这种结构的工具,模型会毫不犹豫地选择正确 action。
这些架构层面的节省,在动态工具激活或提示注入接入系统之前就已经生效。
原则二:在 Pi 中动态激活工具
来自 denkr.ai 的经验
数月前在开发我的移动应用 denkr.ai 时,我遇到了完全相同的瓶颈。
在移动端工作流中,上下文膨胀会直接损害延迟表现和单位经济效益。我在 Denkr 中构建了一个早期版本的动态工具路由机制,根据意图按上下文加载工具。这证明,只要指令明确、接口无摩擦,模型在需要时激活工具完全没有问题。
当我将 Pi 切换为日常使用的编程 Agent 时,我希望拥有同样精简的配置。由于 Pi 是开源的,并且让开发者能够完全控制运行时生命周期钩子,实现这一点非常直接。
动态工具激活的工作方式
该扩展(dynamic-tools)围绕四项核心机制运行。
English / 原文
When you start a session in a modern AI coding agent, a huge chunk of your context window is consumed before you type your first message.
Between system instructions, formatting rules, MCP server integrations, and dozens of registered tool schemas, most agent harnesses dump 10,000 to 25,000+ tokens of static overhead into the context window on every turn.
On 90% of turns, an agent only needs basic file and shell tools (read, bash, edit, write). Specialized tools like browser automation, image generation, web search, or background task runners are needed occasionally, sometimes only once a week.
Leaving 25 to 80+ tool definitions active in the LLM function schema 100% of the time wastes tokens, increases latency, and degrades model reasoning by polluting the attention space with irrelevant parameters.
I solved this with two design decisions:
- Action-based tool consolidation. Structuring custom tools from day one to avoid CRUD schema duplication.
- Dynamic tool activation in Pi. Keeping a baseline of 4 tools active, placing everything else on standby, and letting the model or the user activate tools on demand with zero meta-tool schema overhead and automatic TTL cleanup.
Benchmarking turn zero context across agent harnesses
To measure the scale of the problem, I tested how different coding agent harnesses handle tool schemas and context on a fresh session by sending a single greeting: "hi".
1. Codex: 79 tools and 14.5k tokens by default
I turned off every external plugin and MCP server in Codex, leaving only two custom skills alongside the default built-in setup. Then, I started a fresh session and sent "hi".
The model answered with a standard one-line greeting ("Hi! How can I help?"). The thread status showed that the session had already consumed 14,534 tokens (6% of the 258k context window gone on turn zero).
Figure 1: Codex context consumption after sending a single "hi". 14,534 tokens consumed before any actual work begins.
When I asked the agent which tools were currently active and callable, it returned 79 active tools.
Click to view the full list of 79 active tools loaded in Codex
apply_patch
codex_app__automation_update
codex_app__create_thread
codex_app__fork_thread
codex_app__get_handoff_status
codex_app__handoff_thread
codex_app__list_archived_threads
codex_app__list_projects
codex_app__list_threads
codex_app__load_workspace_dependencies
codex_app__navigate_to_codex_page
codex_app__open_in_codex
codex_app__read_thread
codex_app__read_thread_terminal
codex_app__send_message_to_thread
codex_app__set_thread_archived
codex_app__set_thread_pinned
codex_app__set_thread_title
codex_app__share_thread
codex_app__wait_threads
create_goal
exec_command
get_goal
image_gen__imagegen
list_available_plugins_to_install
list_mcp_resource_templates
list_mcp_resources
mcp__codex_apps__codex_document_control_execute_document_command
mcp__codex_apps__codex_document_control_get_document_tool_schemas
mcp__codex_apps__codex_document_control_list_document_sessions
mcp__codex_apps__plugin_management_get_app_permissions
mcp__codex_apps__plugin_management_get_plugin_dependencies
mcp__codex_apps__plugin_management_uninstall_app
mcp__codex_apps__plugin_management_update_app_permissions
mcp__codex_apps__safety_settings_get_family_info
mcp__codex_apps__safety_settings_get_parental_controls
mcp__codex_apps__safety_settings_get_trusted_contact
mcp__codex_apps__safety_settings_prepare_parental_control_update
mcp__codex_apps__safety_settings_update_parental_control
mcp__codex_apps__sites_add_custom_domain
mcp__codex_apps__sites_change_site_slug
mcp__codex_apps__sites_create_site
mcp__codex_apps__sites_create_source_repository_write_credential
mcp__codex_apps__sites_deploy_private_site_version
mcp__codex_apps__sites_deploy_site_version
mcp__codex_apps__sites_generate_siwc_bypass_token
mcp__codex_apps__sites_get_deployment_status
mcp__codex_apps__sites_get_environment_variables
mcp__codex_apps__sites_get_site
mcp__codex_apps__sites_get_site_version
mcp__codex_apps__sites_get_site_worker_logs
mcp__codex_apps__sites_list_custom_domains
mcp__codex_apps__sites_list_site_versions
mcp__codex_apps__sites_list_sites
mcp__codex_apps__sites_read_database_overview
mcp__codex_apps__sites_read_database_table_rows
mcp__codex_apps__sites_refresh_custom_domain_status
mcp__codex_apps__sites_remove_custom_domain
mcp__codex_apps__sites_save_site_version
mcp__codex_apps__sites_update_environment_variables
mcp__codex_apps__sites_update_site_access
mcp__codex_apps__sites_update_site_metadata
mcp__node_repl__js
mcp__node_repl__js_add_node_module_dir
mcp__node_repl__js_reset
multi_agent_v1__close_agent
multi_agent_v1__resume_agent
multi_agent_v1__send_input
multi_agent_v1__spawn_agent
multi_agent_v1__wait_agent
plugin_management__uninstall_plugin
read_mcp_resource
request_permissions
request_plugin_install
update_goal
update_plan
view_image
web__run
write_stdin
If you enable just one or two extra plugins, such as security scanners or GPT apps, the active tool list passes 100 callable tools.
2. Gemini and Antigravity: fewer tools, still 19.9k tokens
You might assume that keeping the tool list shorter avoids context bloat. The Antigravity CLI (agy) with Gemini 3.7 Flash shows that tool count alone is not the whole story.
I ran the exact same test: I opened a fresh session and sent "hi".
The response was a single line ("Hello! How can I help you with your project today?"). The telemetry reported that 19.9k tokens were consumed immediately on turn zero, with 13.8k tokens taken up by tool schemas alone.
Figure 2: Antigravity CLI context breakdown after sending a single "hi". 13.8k tokens consumed by tool schemas alone.
Antigravity had only 17 tools active, not 79. Yet its tool schemas consumed 13.8k tokens by themselves.
Click to view the 17 active tools in Antigravity
1. run_command
2. manage_task
3. schedule
4. define_subagent
5. invoke_subagent
6. manage_subagents
7. send_message
8. write_to_file
9. replace_file_content
10. view_file
11. list_dir
12. grep_search
13. find_by_name
14. search_web
15. read_url_content
16. generate_image
17. ask_question
The redundancy problem
Why create dedicated LLM tools for list_dir, grep_search, and find_by_name when the agent already has run_command (native bash)?
An agent with bash access runs ls, grep, rg, or find directly. Building separate function schemas for basic shell operations duplicates capabilities the model already has, while adding thousands of tokens of JSON schema definitions, parameter documentation, and edge-case instructions to every turn.
3. Claude Code: built-in baseline and the MCP dilemma
Anthropic recognized this problem in Claude Code and introduced Deferred Tool Loading.
Out of the box without any MCP servers, Claude Code maintains a baseline of around 8 to 10 built-in tools (Bash, View/Read, Edit, Replace, Glob, Grep, Agent, WebSearch, NotebookEdit). Combined with system prompts and project instructions, turn zero consumption sits around 3,500 to 5,000+ tokens.
When developers connect multiple MCP servers for databases, issue trackers, or browser automation, the tool registry passes 30 to 40+ tools , pushing the tool schema payload alone to over 10,000 to 14,000+ tokens per turn.
To handle this, Claude Code splits tools into two tiers when deferrable definitions exceed 10% of the context window:
- Always Loaded: Core file and search tools plus infrastructure (
Bash,Read,Edit,Write,Glob,Grep,Agent,ToolSearch,Skill). - Deferred (Name-only):
WebSearch,NotebookEdit, cron automation tools, and all connected MCP extension tools.
Always Loaded:
Bash, Read, Edit, Write, Glob, Grep, Agent, ToolSearch, Skill
Deferred (Names only until fetched):
WebSearch, TodoWrite, NotebookEdit, CronCreate, MCP servers...
While deferred loading reduces turn zero bloat when many MCPs are active, its discovery mechanism relies on an LLM meta-tool (ToolSearch):
- When the model needs a deferred tool, it must first execute
ToolSearch("select:ToolName"). - The backend injects the full schema into the context, and only on the following turn can the model execute the tool.
- To prevent the model from forgetting loaded tools during context compaction, the runtime maintains custom boundary metadata and compaction recovery logic.
- Even in its minimal state, Claude Code keeps 9 tools permanently loaded (including redundant search tools and the
ToolSearchmeta-tool itself), maintaining a baseline overhead of several thousand tokens.
*Note on Claude Code numbers: Because I do not use Claude Code personally, these figures are based on technical analyses, telemetry shared by other engineers, and community discussions online. I used the lower-bound estimates reported by active users across standard setups.
Why turn zero overhead degrades agent performance
This design pattern across modern harnesses creates two problems:
- Token cost and context exhaustion. Burning 14k to 20k tokens on turn zero means you hit context limits and rate quotas faster. Over a multi-turn session with long reasoning chains, you re-send those 17 to 80+ tool schemas on every single request.
- Attention dilution and tool confusion. Models perform best when their decision space is focused. When an LLM sees dozens of similar tools (multiple thread management endpoints, site deployment tools, multi-agent spawners, duplicate search utilities), it burns reasoning capacity sifting through irrelevant options and is more prone to parameter hallucinations or picking the wrong tool.
Principle 1: action consolidation instead of CRUD APIs
Cutting context bloat does not start with runtime tricks. It starts with how you design individual tools from day one.
In traditional software engineering, REST and CRUD principles encourage creating granular endpoints for every verb:
memory_readmemory_writememory_updatememory_delete
This makes sense for HTTP APIs because registering an extra endpoint in code has zero runtime payload cost until a client makes a request.
For AI agents, that assumption fails completely. Every tool schema is sent across the wire and loaded into the LLM context window on every turn. Four separate CRUD tools mean four JSON headers, four descriptions, four parameter objects, and four entries crowding the model's decision space.
Action-based tool consolidation
When I design custom tools for agents, I consolidate operations by intent. For memory, I split the interface into at most two tools:
memory_read: Handles semantic search, keyword lookup, and fetching specific memories.memory_write: Handles storing new memories, updating existing entries, and deleting memories by passing anactionparameter ("create" | "update" | "delete").
{
"name": "memory_write",
"description": "Create, update, or delete entries in agent memory.",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["create", "update", "delete"],
"description": "The mutation action to perform."
},
"id": {
"type": "string",
"description": "Memory ID (required for update or delete)."
},
"content": {
"type": "string",
"description": "Memory content (required for create or update)."
}
},
"required": ["action"]
}
}
The schema for memory_write is only about 15% to 20% larger than a single memory_create schema, but it replaces three separate tool definitions with one. You cut the schema footprint by 50% without losing any functionality.
If I want to be even more aggressive with token efficiency, I collapse all memory interactions into a single memory tool with an action enum (search, fetch, create, update, delete).
Modern LLMs handle action-parameterized tools reliably. I have run tools structured this way for months across diverse tasks, and the models pick the correct action without hesitation.
These architectural savings take effect before dynamic tool activation or prompt injection ever touches the system.
Principle 2: dynamic tool activation in Pi
Lessons from denkr.ai
I ran into this exact bottleneck months ago when building my mobile app, denkr.ai.
On mobile workflows, context bloat directly degrades latency and unit economics. I built an early variation of dynamic tool routing in Denkr, loading tools contextually based on intent. It proved that models have no issue activating tools when they need them, provided the instructions are clear and the interface is frictionless.
When I switched to Pi as my daily coding agent, I wanted the same lean setup. Because Pi is open source and gives developers full control over runtime lifecycle hooks, building this was straightforward.
How dynamic tool activation works
The extension (dynamic-tools) operates on four core mechanics:
- A hard default of 4 active tools (
read,bash,edit,write). - Standby tool registration with zero-schema prompt injection.
- In-process bash interception for activation (
pi-tool). - Co-activation groups and automatic TTL pruning.
+-------------------------------------------------------------+
| User Prompt |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| Pi Hook: before_agent_start |
| - Active tools set to: read, bash, edit, write |
| - Injects minimal Markdown standby tool list into prompt |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| LLM decides to use a standby tool |
| Runs bash: pi-tool activate browser_use |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| Pi Hook: tool_call |
| - Intercepts bash command in-process |
| - Calls pi.setActiveTools([...defaults, ...browserTools]) |
| - Rewrites bash command to safe stdout echo |
+-------------------------------------------------------------+
|
v
+-------------------------------------------------------------+
| Pi Hook: agent_settled |
| - Decrements run TTL on non-default tools |
| - Automatically purges expired tools back to 4 defaults |
+-------------------------------------------------------------+
Technical mechanisms under the hood
1. Minimal default baseline (4 core tools)
At startup, the extension forces Pi's active tool schema to only 4 tools:
const DEFAULT_CONFIG = {
defaultTools: ["read", "bash", "edit", "write"],
groups: {
web_search: ["web_search", "web_fetch"],
browser_use: [
"browser_open",
"browser_observe",
"browser_preview",
"browser_diagnostics",
"browser_act",
"browser_wait",
],
loops: ["loops_report", "loops_create_definition", "loops_create_job", "loops_inspect"],
},
autoResetOnSessionStart: true,
toolTtlRuns: 2,
};
Any extension registered in Pi (via plugins, MCP, or local scripts) is loaded by Pi internally, but excluded from the active LLM schema using pi.setActiveTools(...).
2. Zero-schema overhead: prompt injection instead of meta-tools
A common mistake when building tool managers is creating a dedicated LLM meta-tool (like activate_tool({ name: string })).
Adding a meta-tool adds its own JSON schema overhead, parameter documentation, and function-calling indirection.
Instead, I use Pi's before_agent_start hook to append a plain, lightweight Markdown summary to the system prompt:
pi.on("before_agent_start", async (event, _ctx) => {
await loadConfig();
applyActiveTools();
const standby = getStandbyTools();
if (standby.length === 0) return;
const standbyLines = standby.map((name) => {
const group = findGroupForTool(name, config.groups);
return group ? `- \`${name}\` (part of group: **${group}**)` : `- \`${name}\``;
});
const injection = [
"## Dynamic Tool Activation",
`Default active tools: ${config.defaultTools.map((t) => `\`${t}\``).join(", ")}.`,
"Standby tools (not currently in your active schema):",
...standbyLines,
"",
"To activate a tool or group, run in bash: `pi-tool activate <name>`",
].join("\n");
return {
systemPrompt: `${event.systemPrompt}\n${injection}`,
};
});
A raw text list of 20 tool names takes around 100 tokens. In contrast, 20 JSON tool schemas with parameters, types, and descriptions consume 4,000 to 10,000 tokens.
3. In-process bash interception
Since the agent already has bash enabled by default, it does not need a new tool to activate others. It simply runs:
pi-tool activate browser_use
The extension intercepts the bash call directly inside the Node.js process using pi.on("tool_call"):
pi.on("tool_call", async (event, _ctx) => {
if (event.toolName !== "bash") return;
const command = event.input?.command;
if (typeof command !== "string") return;
const piToolMatch = command.trim().match(/^(?:pi-tools?|activate-tool)(?:\s+(.*))?$/i);
if (!piToolMatch) return;
// Activate the tools in memory
const result = activateTools(targetArgs);
const outputMessage = `[dynamic-tools] Activated tool(s): ${result.activated.join(", ")}`;
// Rewrite command on the fly so bash executes an immediate echo with exit code 0
const escaped = JSON.stringify(outputMessage);
event.input.command = `node -e 'console.log(${escaped})'`;
});
There is no separate CLI binary installed on the host machine. Pi catches the call, updates pi.setActiveTools(), rewrites the shell command to output the confirmation string, and returns cleanly to the model. On the next turn, the activated tools are in the JSON schema.
4. Co-activation groups
Tools rarely exist in isolation. When the agent needs browser automation, it needs navigation, observation, and action tools together (browser_open, browser_observe, browser_act, browser_wait).
Grouping them in config.json allows a single call like pi-tool activate browser_use (or activating any single tool inside that group) to bring in the entire bundle at once.
5. Automatic TTL pruning
Once a specialized task is finished, those extra tools should not linger in the prompt for the rest of the day.
The extension assigns a Time-To-Live (TTL) counter (default: 2 runs) to every activated standby tool.
pi.on("agent_settled", async (_event, _ctx) => {
let changed = false;
for (const [tool, ttl] of Array.from(toolTtlMap.entries())) {
const nextTtl = ttl - 1;
if (nextTtl <= 0) {
activeToolNames.delete(tool);
toolTtlMap.delete(tool);
changed = true;
} else {
toolTtlMap.set(tool, nextTtl);
}
}
if (changed) {
applyActiveTools();
}
});
If the agent executes an activated tool during a turn, its TTL refreshes. When the agent returns to standard coding tasks and leaves the tool idle for 2 turns, the tool expires and context snaps back to the lean 4-tool baseline.
6. Real-world telemetry in CircaCode
Here is what this looks like on a fresh turn zero greeting ("hi"):
Figure 3: Turn zero context consumption in Pi running inside CircaCode. Only 3.6k tokens processed (3.5k input tokens, 1.3% of the 272k window), including system prompt, persona, skills, and dynamic standby hints.
Note: This image was captured from inside my desktop application,CircaCode , which runs Pi as its agent engine.
7. Interactive user slash commands
For human control, the extension registers native slash commands:
/activate-tool <name>/deactivate-tool <name>/tool list/tool reset
These include auto-completion and interactive terminal select menus (ctx.ui.select), giving both the human and the agent equal control over the active workspace.
Side-by-side comparison
Here is how all four coding agent environments compare when sending a single "hi" on turn zero:
| Agent Harness | Default Active Tools | Turn 0 Baseline Context Consumed | Tool Activation Mechanism |
|---|---|---|---|
| Codex | 79 tools (100+ with plugins) | 14,534 tokens (6.0% of 258k) | Static (all tools always active in schema) |
| Antigravity (Gemini 3.7) | 17 tools | 19,900 tokens (13.8k tools alone) | Static (all tools always active in schema) |
| Claude Code* | 8â10 built-in (30â40+ with MCP) | ~3,500 â 14,000+ tokens | Meta-tool (ToolSearch round-trip for deferred tools) |
| Pi + Dynamic Tools | 4 core tools (read, bash, edit, write) |
3,600 tokens (1.3% of 272k) | In-process bash interception + auto-TTL |
*Claude Code numbers represent conservative estimates reported by developers and technical documentation online, rather than personal benchmarks.
Results and takeaways
| Metric | Static Tools Baseline (Codex / Antigravity) | Pi + Dynamic Tool Activation | Reduction |
|---|---|---|---|
| Turn 0 Context Overhead | 14,500 â 19,900 tokens | 3,600 tokens | -75% to -82% |
| Active Schema Tools | 17 â 79+ tools | 4 core tools | -76% to -95% |
| Tool Selection Reliability | Susceptible to schema hallucinations | High precision | Clean decision space |
1. Preserving model intelligence and attention
LLMs are sharper when their context window is clean. As the context window fills past 40% to 50% capacity, attention degrades. Models miss subtle instructions, make syntax errors, and produce more tool hallucinations.
Starting every session with 15k to 20k tokens of static tool schemas eats directly into that high-performance zone. By keeping turn zero at 3.6k tokens, the model stays in its sharpest reasoning state for much longer during complex coding sessions.
2. Building and connecting tools without context anxiety
Before dynamic activation, adding a new tool was an architectural compromise. Every new tool meant asking: "Is this feature useful enough to justify permanently adding 500 tokens of JSON schema to every single prompt for the rest of time?"
With dynamic activation, that penalty disappears. I can build, connect, and experiment with whatever tools and extensions I want, including browser automation, background task loops, scrapers, voice synthesis, or custom database utilities.
When a tool is on standby, it does not load a JSON schema. It costs a single line of plain text in the standby hints list (about 5 to 10 tokens). I get an extensive tool ecosystem without paying the context tax on turns where I just need to edit a file.
3. Open agent runtimes win
Harnesses that treat the LLM context as an open, hackable runtime make this kind of optimization possible. Having direct access to lifecycle hooks (before_agent_start, tool_call, agent_settled) lets you tailor the agent environment to your actual workflow instead of being locked into a rigid, static tool schema.