Pi HTTP开发

@evalexp/pi-http为Pi增加一个http Tool,让Agent直接执行HTTP(S)请求,不需要临时生成curlwget、PowerShell或脚本。

项目:evalexp/pi-http

通用Extension机制直接参考Pi Extension。这里只记录pi-http自身的实现、设计取舍和维护注意点。

设计目标

这个Extension解决的不是“缺少HTTP Client”,而是Agent缺少稳定、结构化、跨平台的HTTP执行能力

让Agent退回Shell执行HTTP会带来额外不确定性:

  • curlwget不保证存在;
  • Windows和Unix命令不同;
  • Shell quoting容易破坏JSON、Header和Multipart参数;
  • 二进制响应不适合直接经过终端再进入Context;
  • Shell输出缺少稳定结构;
  • Agent会额外消耗Token生成命令和解析输出。

因此这里把HTTP能力收敛成一个Pi Tool:

flowchart LR
    A[Agent表达HTTP意图] --> B[http Tool]
    B --> C[结构化参数]
    C --> D[HTTP执行层]
    D --> E{响应处理}
    E -->|普通响应| F[文本 / Base64]
    E -->|下载| G[直接流式落盘]
    F --> H[Tool Result]
    G --> H

设计原则:

  1. Agent只表达请求,不拼Shell;
  2. Pi入口和HTTP实现解耦;
  3. HTTP Status和Tool执行错误分离;
  4. 普通Response必须限制进入Context的大小;
  5. 文件下载直接落盘;
  6. 下载失败不能留下Partial File;
  7. 默认不覆盖本地文件;
  8. 请求同时支持Timeout和Pi取消;
  9. 尽量兼容Node和Bun运行时。

项目结构

当前仓库很小,业务边界也比较明确:

文件职责
index.tsPi Tool定义、Schema、Prompt规则、最终Context截断
request.ts请求构造、HTTP执行、Response解析、下载、TLS、错误分类
request.test.tsHTTP执行层的本地HTTP/HTTPS集成测试
test-fixtures/自签名HTTPS测试证书和私钥
package.jsonPi Package、依赖、测试和发布约束
.gitignore本地开发产物和环境文件忽略规则
README.md对外安装、参数、响应格式和安全说明

核心依赖关系:

flowchart TD
    PI[Pi Runtime] --> INDEX[index.ts]
    INDEX --> TOOL[http Tool]
    TOOL --> REQUEST[executeHttpRequest]
    REQUEST --> UNDICI[undici]
    REQUEST --> FS[Node File API]

    TEST[request.test.ts] --> REQUEST
    TEST --> HTTP[Local HTTP Server]
    TEST --> HTTPS[Local HTTPS Server]
    HTTPS --> TLS[test-fixtures]

这里最重要的是request.ts不依赖Pi运行时对象

ExtensionAPIctx、TUI、Session等只存在于index.ts,因此请求执行层可以直接使用Node Test Runner测试。

Pi Extension#项目结构里“入口薄、业务实现独立”的实际应用。

分层

可以把代码分成两层。

文件关注点
Pi Adapterindex.tsTool契约、LLM Prompt、Context预算
HTTP Corerequest.tsHTTP和本地文件语义

Pi Adapter

index.ts只需要回答:

  • Agent能传什么参数;
  • 什么时候应该调用这个Tool;
  • 如何调用HTTP Core;
  • 最终多少内容允许进入Context;
  • 哪些结构化数据放details

HTTP Core

request.ts负责:

  • 参数组合语义;
  • Body构造;
  • 请求取消和Timeout;
  • TLS行为;
  • Response Header保真;
  • Response Body读取;
  • 文本/二进制编码;
  • 下载事务;
  • 错误分类。

这样以后即使Pi Tool接口调整,HTTP实现基本不用动;反过来替换HTTP Client时,也不需要重写Tool Schema。

Tool入口

Tool名称是http,入口使用Pi的defineTool()构造。

参数主要分成四组:

类别参数
基础请求urlmethodheaders
Bodybodyjsonformfiles
连接timeoutMsinsecure
输出maxBytesdownloadPathoverwrite

Schema禁止额外字段:

Type.Object(properties, {
    additionalProperties: false,
});

这里主要是限制模型自由发挥参数名,避免Tool层收到“看起来合理但实际上没有实现”的选项。

通用Tool定义参考Pi Extension#Tool开发

Prompt约束

Tool除了description,还提供了promptGuidelines

规则核心是:

  • 直接HTTP(S)请求优先调用http
  • 默认不要生成Shell HTTP命令;
  • 只有用户明确要求其它客户端、Tool表达不了请求、或已有专用搜索/内容提取工具时才绕过;
  • 下载文件时使用downloadPath
  • overwrite只在确实需要替换文件时启用。

这部分很重要,因为Extension不是单纯“把Tool注册进去”,还需要告诉Agent能力边界和选择优先级

参考Pi Extension#Tool描述和Prompt

请求执行总流程

executeHttpRequest()是HTTP Core的Orchestrator:

flowchart TD
    A[executeHttpRequest] --> B[校验Body / Download参数]
    B --> C[创建Headers]
    C --> D[构造Request Body]
    D --> E[推断Method]
    E --> F[组合Timeout和外部AbortSignal]
    F --> G{insecure?}
    G -->|否| H[undici fetch]
    G -->|是| I[配置TLS]
    I --> H

    H --> J{downloadPath?}
    J -->|是| K[Response流式写文件]
    J -->|否| L[按maxBytes读取Body]

    K --> M[格式化Download Result]
    L --> N[识别文本 / 二进制]
    N --> O[格式化Response Result]

    M --> P[返回index.ts]
    O --> P
    P --> Q[Pi级二次截断]

请求层返回统一的:

interface HttpExecutionResult {
    text: string;
    details: HttpDetails;
}

text给LLM直接读,details保留结构化状态。

Body参数

支持四种Body输入:

参数实际编码
bodyRaw String
jsonJSON
formURL Encoded Form
filesMultipart Form

互斥关系

Body参数不是任意组合:

组合允许
body
json
form
files
form + files
body + json
body + form
json + form
files + body
files + json

这种跨字段约束没有硬塞进TypeBox,而是在HTTP Core里单独校验。

这样做的好处:

  • 规则直观;
  • 错误信息容易稳定;
  • HTTP Core即使脱离Pi调用,也保留自己的业务约束。

Presence判断

实现里不是简单判断值是否Truthy,而是判断字段是否实际存在

这个细节保证了下面这些合法值不会被误判成“没传”:

  • 空字符串Body;
  • false
  • 0
  • null JSON;
  • 空对象。

这是构造通用请求参数时比较值得保留的做法。

Method推断

如果显式提供method,优先使用。

没有提供时:

最终BodyMethod
GET
POST

因此JSON、Form和Multipart都会自然得到POST

Method只是做最小推断,没有自己维护完整HTTP方法白名单。

协议是否合法继续交给Undici处理,避免Tool层重新实现HTTP规范。

JSON

JSON模式:

  1. 序列化为JSON;
  2. 没有显式Content-Type时补application/json
  3. 用户手动提供的Header优先。

设计规则是:

自动默认值只能补缺省值,不能覆盖调用方显式配置。

URL Encoded Form

form使用URL Encoded编码。

默认Content-Type为:

application/x-www-form-urlencoded;charset=UTF-8

如果用户已经提供Content-Type,不会覆盖。

Multipart

存在files后切换到Multipart:

flowchart TD
    A[files存在] --> B[创建FormData]
    B --> C[加入form字段]
    C --> D[遍历files]
    D --> E[解析本地路径]
    E --> F[读取文件]
    F --> G[创建Blob]
    G --> H[追加文件字段]
    H --> I[交给Undici发送]

files类型是:

Record<string, string>

含义:

部分含义
KeyMultipart字段名
Value本地文件路径

例如概念上:

files: {
    file: "./report.txt",
}

文件路径

相对路径基于Pi当前cwd解析。

同时接受@前缀:

输入处理
./report.txt正常解析
@./report.txt去掉@后解析

这里的@只是为了让调用习惯更接近常见CLI,并没有引入新的文件引用协议。

Multipart的当前取舍

上传文件使用readFile()整体读入内存,再包装成Blob

优点:

  • 实现简单;
  • 和FormData接口契合;
  • 测试容易;
  • 小文件上传足够稳定。

限制:

  • 大文件上传会占用明显内存;
  • 没有上传进度;
  • 不是Streaming Upload。

如果以后支持大文件,优先考虑重构这里。

另外Multipart Header最好继续交给Undici自动生成Boundary;调用方手动写Multipart Content-Type时要特别谨慎。

Timeout和取消

请求有两个取消来源:

来源作用
Pi传入AbortSignal用户或Agent中止Tool
timeoutMs请求自身超时

两个Signal会组合成请求Signal:

flowchart LR
    A[Pi AbortSignal] --> C[Combined Signal]
    B[Timeout Signal] --> C
    C --> D[undici fetch]

异常发生后再检查是哪一个Signal触发。

分类逻辑:

flowchart TD
    A[请求抛出异常] --> B{Timeout触发且外部未Abort?}
    B -->|是| C[HTTP_TIMEOUT]
    B -->|否| D{外部Signal Abort?}
    D -->|是| E[HTTP_ABORTED]
    D -->|否| F[HTTP_REQUEST_FAILED]

这里比直接把Undici异常原样抛给Agent更稳定。

特别是Timeout不只覆盖“等待响应头”,测试也覆盖了响应头已经返回,但Body迟迟未结束的情况。

TLS

insecure=true关闭证书校验。

实现专门区分Node和Bun:

Runtime实现方式
Bun请求TLS Option
Node / Undici临时创建自定义Agent
flowchart TD
    A[insecure=true] --> B{Bun?}
    B -->|是| C[TLS rejectUnauthorized=false]
    B -->|否| D[创建Undici Agent]
    D --> E[Agent connect.rejectUnauthorized=false]
    C --> F[发送请求]
    E --> F

实现没有把Pi宿主环境硬编码成Node。

Dispatcher清理

Node分支创建的Agent只服务当前请求。

无论成功失败,最后都会尝试关闭。

如果清理自身失败,不会覆盖真正的HTTP执行结果。

这里应该保持:

Cleanup Error不能替代Primary Error。

测试使用本地自签名HTTPS服务器验证:

  • 默认请求失败;
  • insecure=true后成功。

Response Header

Response Header最终保存为数组,而不是普通Object:

interface HttpHeader {
    name: string;
    value: string;
}

原因是Header可能重复。

最典型的是:

Set-Cookie

实现会单独读取重复Cookie,再逐条放入结果。

如果使用Record<string, string>,多个Cookie会发生信息损失。

所以details.headers保持List语义是刻意设计,不应该随意简化成Object。

普通Response读取

没有downloadPath时,Response Body需要进入Context。

这里没有直接一次性读取完整Body,而是读取Stream并在达到上限后停止:

flowchart TD
    A[Response Stream] --> B[读取Chunk]
    B --> C{达到maxBytes?}
    C -->|否| D[记录Chunk]
    D --> B
    C -->|是| E[保留允许的部分]
    E --> F[Cancel Reader]
    B -->|EOF| G[合并结果]
    F --> G

这样可以在网络读取阶段控制内存和Context,而不是先把巨大Response完整读完再截断。

maxBytes

maxBytes表示原始响应Body字节限制

它不等于:

  • 字符数量;
  • Base64编码后的长度;
  • 最终Tool Result总大小。

Tool Schema把最大值限制在Pi的默认最大输出字节数以内。

文本和二进制

读取Body后,通过Content-Type判断编码方式。

视为文本的主要类型:

  • Content-Type缺失;
  • text/*
  • JSON;
  • XML;
  • JavaScript;
  • URL Encoded;
  • SVG。

其它类型按二进制处理并转Base64。

flowchart LR
    A[Body Bytes] --> B{Text-like Content-Type?}
    B -->|是| C[UTF-8 Decode]
    B -->|否| D[Base64]
    C --> E[Response Text]
    D --> E

这个策略是基于Content-Type的启发式处理

当前没有:

  • MIME sniffing;
  • charset转换;
  • gzip等协议层手工处理。

这些继续交给Fetch/服务端Header语义。

因此如果服务端错误声明MIME,Tool也可能采用错误编码方式。

HTTP Status不是Tool Error

实现没有使用response.ok决定是否抛异常。

因此:

  • 400;
  • 401;
  • 404;
  • 500;

都会正常返回Status、Header和Body。

flowchart LR
    A[HTTP 500] --> B[有效HTTP Response]
    B --> C[保留诊断Body]
    C --> D[Agent继续分析]

这是Agent HTTP Tool比较重要的边界:

HTTP Error属于协议结果;Transport / Local Error才属于Tool执行错误。

如果直接对所有非2xx抛异常,Agent反而拿不到服务端的诊断正文。

下载模式也沿用同样语义,所以404/500的响应体仍可能被写到downloadPath;调用方必须查看返回的HTTP Status,而不能只根据“文件写成功”判断业务请求成功。

错误分类

当前使用稳定字符串前缀分类,而不是自定义Error Class:

Prefix含义
HTTP_INVALID_BODYBody参数组合非法
HTTP_INVALID_DOWNLOAD下载参数非法
HTTP_FILE_ERROR上传文件读取失败
HTTP_DOWNLOAD_FAILED下载本地写入失败
HTTP_TIMEOUT请求超时
HTTP_ABORTEDTool被取消
HTTP_REQUEST_FAILED其它网络/TLS/客户端错误

这种方案目前足够:

  • Agent容易读;
  • Test容易断言;
  • 不需要维护Error继承树;
  • 以后需要程序化处理时还能升级。

Error边界

校验和上传文件读取发生在Fetch主try之前,因此这些业务错误不会被重新包装成通用网络错误。

下载错误发生在Fetch流程内部,所以会单独保留HTTP_DOWNLOAD_FAILED

Timeout和Abort优先根据Signal重新分类,避免被下载层或Undici错误文本掩盖。

Response格式

普通Response返回:

  1. HTTP Status;
  2. 最终URL;
  3. Body Metadata;
  4. Headers;
  5. Body。

Body Metadata至少包含:

字段含义
encodingutf-8base64file
bytes已读取/写入的原始Body字节数
truncatedBody或最终Tool文本是否发生截断

同时保存结构化details

flowchart LR
    A[HTTP Result] --> B[text]
    A --> C[details]
    B --> D[LLM阅读]
    C --> E[结构化状态]

这样不需要让Agent从自然语言里反解析Status等字段。

参考Pi Extension#content和details

两层截断

这里有两种不同的限制。

HTTP Body限制

request.ts发生:

flowchart LR
    A[Network Stream] --> B[Body Reader]
    B -->|maxBytes| C[有限Body]

解决:

  • 网络响应过大;
  • 内存增长;
  • Body Context占用。

Tool Result限制

回到index.ts以后,再使用Pi的truncateHead()限制完整文本:

flowchart LR
    A[Status + URL + Headers + Body] --> B[truncateHead]
    B --> C[DEFAULT_MAX_BYTES]
    B --> D[DEFAULT_MAX_LINES]
    C --> E[最终Tool content]
    D --> E

解决:

  • Header很多;
  • Base64膨胀;
  • Body虽然有限,但整个输出仍可能超限;
  • 行数过多。

因此:

maxBytes是HTTP Body预算,不是最终Context预算。

对应Pi Extension#输出截断里的“业务层限制 + Tool层最终兜底”。

Base64膨胀

二进制经过Base64后文本体积会增长,所以第二层截断仍然有必要:

flowchart LR
    A[Binary Body] --> B[maxBytes限制原始字节]
    B --> C[Base64编码]
    C --> D[Tool级再次截断]

truncated同步

如果第二层截断发生:

  • details.body.truncated会改成true
  • 文本里的Body Metadata也同步改成true

避免结构化数据和Agent看到的文本状态不一致。

文件下载

存在downloadPath时,响应Body不进入模型Context。

flowchart TD
    A[HTTP Response Stream] --> B[本地File Handle]
    B --> C[逐Chunk写入]
    C --> D[记录总字节数]
    D --> E[返回Path + Metadata]

下载模式:

  • 不受Body maxBytes限制;
  • 不进行Base64;
  • 自动创建父目录;
  • 默认不覆盖已有文件;
  • 失败时清理Partial File。

下载事务

下载是实现里最值得记录的文件设计。

默认不覆盖

overwrite=false时直接独占创建最终文件:

flowchart TD
    A[目标路径] --> B[Exclusive Create]
    B -->|不存在| C[开始写入]
    B -->|已存在| D[HTTP_DOWNLOAD_FAILED]
    C --> E{完整完成?}
    E -->|是| F[保留文件]
    E -->|否| G[删除Partial File]

因此默认不会因为Agent一次Tool调用直接覆盖用户现有文件。

overwrite=true

允许覆盖时,不直接写原文件:

flowchart TD
    A[目标文件] --> B[同目录创建随机临时文件]
    B --> C[完整下载到临时文件]
    C --> D{下载成功?}
    D -->|否| E[删除临时文件]
    D -->|是| F[Rename到最终路径]

原文件在下载完成前保持不变。

这个设计可以看成简化的Transactional Write。

临时文件和目标文件位于同一目录,也避免跨文件系统Rename的问题。

Partial Write

底层写Chunk时没有假设一次write()一定写完整。

逻辑会持续写到整个Chunk全部提交。

这是文件API里容易漏掉的小细节,后面如果重构下载代码不要退化掉。

失败清理

失败路径会:

  • Cancel Response Reader;
  • Close File Handle;
  • 删除已经创建但未完成的目标/临时文件。

Timeout和Abort也会走到清理逻辑。

测试明确验证Timeout时不会留下Partial Download。

并发

Pi Tool可能并发,参考Pi Extension#并行Tool和文件修改

当前文件下载已经提供:

  • Exclusive Create;
  • 随机临时文件;
  • 完成后Rename;
  • 失败清理。

但两个请求如果都:

  • 指向同一个downloadPath
  • 同时设置overwrite=true

仍然属于Last Writer Wins语义。

如果以后需要更严格的“同路径串行修改”,可以在Pi Adapter层按最终路径接入withFileMutationQueue()

当前没有这样做,应该视为一个明确的并发边界,而不是默认认为下载完全串行安全。

测试架构

request.test.ts没有Mock Fetch,而是在测试进程内启动真实HTTP Server。

flowchart TD
    A[node:test before] --> B[创建临时目录]
    B --> C[创建上传Fixture]
    C --> D[启动HTTP Server]
    D --> E[随机本地端口]
    C --> F[读取TLS Fixture]
    F --> G[启动HTTPS Server]
    G --> H[随机本地端口]

    I[Test Cases] --> D
    I --> G

    I --> J[executeHttpRequest]
    J --> D
    J --> G

    K[node:test after] --> L[关闭Server]
    K --> M[删除临时目录]

这种方式介于Unit Test和Integration Test之间,更接近HTTP Core Contract Test

  • 不需要Internet;
  • 不依赖第三方API;
  • 真实经过Socket;
  • 真实经过Undici;
  • 真实操作文件系统;
  • TLS也是真实握手。

相比Mock fetch(),更容易发现Stream、Header、Abort和TLS这类行为差异。

测试覆盖

当前测试覆盖的行为:

场景主要验证
GET默认Method和规范化输出
insecure HTTPS自签名证书默认失败,insecure成功
JSON自动POST、Content-Type、自定义Header
Raw Body自定义Method
URL EncodedForm编码和Content-Type
MultipartForm + File上传
HTTP 500非2xx保持正常Result
Set-Cookie重复Header不丢失
BinaryBase64编码
DownloadBody不进入Result、完整落盘
Overwrite默认拒绝,显式允许替换
maxBytesBody按字节截断
TimeoutHeader前和Body阶段都能超时
Abort外部Signal取消
参数冲突Body和Download组合校验
Partial DownloadTimeout后文件被清理
Missing Upload本地文件错误分类

值得注意的是Timeout测试同时覆盖:

  • 服务端迟迟不返回完整请求结果;
  • 已经发出Response Header但Body迟到。

这能避免只测试连接阶段Timeout。

TLS Fixture

test-fixtures/中的证书和私钥只是公开的测试Fixture,用于本地自签名HTTPS Server。

它们:

  • 不是生产Credential;
  • 只用于验证证书校验开关;
  • 不进入npm发布包。

这里保留Fixture比测试时动态调用OpenSSL更稳定,也减少测试环境依赖。

Package设计

package.json当前关键配置:

配置值/作用
name@evalexp/pi-http
version0.1.1
typeESM
engines.node>=20.18.1
Runtime Dependencyundici
Peer DependencyPi Coding Agent、TypeBox
Dev Dependencytsx
Pi入口./index.ts
Publish AccessPublic

Package直接发布TypeScript源码,没有构建产物。

这依赖Pi本身支持直接加载Extension TypeScript,通用机制参考Pi Extension#TypeScript直接加载

files白名单

npm包只发布运行时必要内容:

  • index.ts
  • request.ts
  • README.md
  • LICENSE

测试代码、TLS Fixture和其它本地文件不会进入最终Package。

这样npm包保持很小,也不会把测试私钥Fixture误当运行时资源发布出去。

依赖分类

undici是运行时真正使用的第三方库,所以放dependencies

Pi和TypeBox由宿主环境提供,所以放peerDependencies

tsx只负责运行测试,所以放devDependencies

这和Pi Extension#依赖管理里的依赖边界一致。

测试和发布

测试脚本:

npm test

底层使用Node Test Runner,通过tsx直接执行TypeScript测试。

发布前还有:

prepublishOnly

因此npm publish前会自动跑测试。

当前发布流程可以理解为:

flowchart LR
    A[修改源码] --> B[npm test]
    B --> C[npm pack --dry-run]
    C --> D[npm publish]
    D --> E[Pi安装/更新验证]

参考Pi Extension#Pi Package

目前仓库没有额外CI Workflow,所以发布质量门主要依赖本地prepublishOnly和手动检查tarball。

.gitignore

仓库忽略:

  • node_modules
  • package-lock.json
  • Coverage;
  • npm tarball;
  • Log;
  • .env*
  • IDE元数据;
  • TypeScript增量构建文件。

其中.env.example允许提交。

当前没有提交Lockfile,因此依赖安装依靠SemVer范围重新解析。

对于这样的小型Extension问题不大,但如果以后依赖明显增多,需要重新考虑测试可复现性。

安全边界

这个Tool能力比较高,因为它同时拥有网络和本地文件权限。

主要风险:

网络

能够访问Pi宿主能访问的URL,包括:

  • localhost;
  • 内网服务;
  • Metadata Endpoint;
  • 管理接口。

所以它天然具备SSRF式能力。

这不是代码漏洞,而是Tool能力本身的权限边界。

上传

files可以读取当前Pi进程权限范围内的本地文件。

Agent生成上传请求时,需要特别关注:

  • Secret;
  • SSH Key;
  • Token;
  • 配置文件;
  • 浏览器/工具Credential。

下载

downloadPath能够创建本地文件。

overwrite=true还能替换已有文件。

默认overwrite=false就是这里最重要的安全默认值。

insecure

insecure=true会绕过TLS证书验证,只应显式使用。

它不能作为“请求失败后的自动重试策略”。

Response

限制Response大小只解决Context预算问题,不代表远端内容可信。

Agent仍可能读取恶意Prompt Injection内容。

设计取舍

不实现完整HTTP客户端UI

没有加入:

  • Cookie Jar;
  • Session;
  • Retry;
  • Proxy配置;
  • Auth Profile;
  • Redirect Policy UI;
  • Cache;
  • Response JSONPath;
  • HAR;
  • 请求历史。

原因是这个Tool定位是Agent基础HTTP能力,不是Postman替代品。

功能越多,Schema越大,模型越难稳定调用。

不对HTTP状态抛异常

因为Agent通常正需要读取错误Body继续分析。

下载与普通Response分开

二进制先Base64再进Context很浪费。

有明确文件目标时直接落盘更合理。

两层截断

Body层解决网络和内存,Tool层解决最终Context。

只做其中一层都不够。

Header使用数组

为了保留重复Header,而不是追求API看起来简单。

Runtime兼容

TLS分支同时考虑Node和Bun,而不是只按标准Node CLI开发。

Error使用Prefix

当前项目规模小,不需要自定义Error Class体系。

当前限制

以后维护时优先记住这些边界。

上传不是流式

Multipart文件全部读入内存。

如果未来需要上传大型镜像、压缩包等,需要重构。

charset固定按UTF-8

文本Response目前没有根据:

charset=...

切换Decoder。

非UTF-8网站可能乱码。

Content-Type依赖服务端

二进制/文本判断是Header启发式,不做内容探测。

Multipart Header由调用方影响

如果调用方手动设置错误的Multipart Content-Type,可能破坏Boundary。

后续可以考虑明确禁止或修正这种配置。

同路径overwrite并发

两个并发下载都允许覆盖时,目前没有文件Mutation Queue。

HTTP状态和下载成功是两个维度

下载404正文也可能成功写文件。

调用方必须检查Status。

没有Retry

网络瞬时失败直接作为Tool Error返回。

是否增加Retry需要谨慎,因为:

  • POST可能不是幂等;
  • 自动Retry可能产生重复副作用;
  • Agent自己可以决定是否重试。

当前不自动Retry是合理默认。

后续改动优先级

如果继续增强,建议按下面顺序考虑:

  1. 大文件Streaming Upload;
  2. charset处理;
  3. 下载路径并发Mutation Queue;
  4. 更明确的Multipart Content-Type保护;
  5. 根据需要增加Proxy;
  6. 最后才考虑Cookie Session或更复杂HTTP Client能力。

不要优先扩充大量Schema字段。

这个Tool的价值在于:

小、稳定、Agent容易正确调用。

维护时的核心约束

以后改代码时,至少保持:

  • index.ts继续薄;
  • request.ts不直接依赖Pi Context;
  • 非2xx继续作为正常HTTP结果;
  • Transport Error继续抛Tool Error;
  • 普通Response始终有限制;
  • 下载不受Body maxBytes截断;
  • 默认禁止覆盖;
  • overwrite下载先写临时文件;
  • 失败不能留下Partial File;
  • 重复Set-Cookie不能丢;
  • Timeout必须覆盖Response Body阶段;
  • Abort和Timeout错误要区分;
  • 临时TLS Dispatcher必须清理;
  • Cleanup失败不能覆盖Primary Error;
  • 测试继续使用本地Server,不依赖公网。

问题快速定位

问题先看
Agent没有选择httpindex.ts Prompt Guidelines
参数Schema不对index.ts HttpParameters
Body组合报错validateBodyInput()
JSON/Form/Multipart异常buildRequestBody()
上传路径异常normalizeFilePath()
Timeout/Abort异常combineSignals()和主Catch
insecure异常TLS Runtime分支
响应被截断readResponseBody() + index.ts truncateHead()
二进制乱码isTextualContentType()
Cookie缺失getResponseHeaders()
下载覆盖/残留downloadResponseBody()
Tool返回格式formatResponse() / formatDownloadResponse()
发布缺文件package.json files
HTTPS测试test-fixtures/

参考