• 简体中文
  • File(文件)

    文件 API 负责文件的读取、写入、编辑、搜索、传输和监听。

    它与 command、terminal、代码执行和浏览器下载共享同一个文件系统。通过文件 API 写入的文件可以立即被 shell 命令看到,反过来也一样。

    工作目录不是隔离边界。文件调用可以访问 daemon 账户有权限访问的任意路径,隔离由沙箱负责。

    在文件 API 中,user 与 command 和 terminal 中的含义不同:

    文件 API 中的 user 只决定新建文件和目录归谁所有;command 和 terminal 中的 user 决定进程以哪个账户运行。

    文件 API 仍以 daemon 自身的权限读写文件。GET /v1/capabilitiesfiles 下报告文件能力,每个操作一个标记。v1 和 v2 路由的对照见 从 1.x 迁移

    文件读写

    curl "$BASE_URL/v2/fs/read?path=/tmp/demo/src/app.py&start_line=2&end_line=5"
    
    curl -X POST "$BASE_URL/v2/fs/write" \
      -H "Content-Type: application/json" \
      -d '{"path": "/tmp/demo/README.md", "content": "# demo\n"}'

    写入请求体示例:

    {
      "path": "/tmp/demo/README.md",
      "content": "# demo\n"
    }

    行号从 0 开始,end_line 不包含在范围内。read 用 query 参数接收 pathstart_lineend_linewrite 用 JSON body,路径字段是 path

    curl -X POST "$BASE_URL/v1/file/read" \
      -H "Content-Type: application/json" \
      -d '{"file": "/tmp/demo/src/app.py", "start_line": 2, "end_line": 5}'
    
    curl -X POST "$BASE_URL/v1/file/write" \
      -H "Content-Type: application/json" \
      -d '{"file": "/tmp/demo/README.md", "content": "# demo\n"}'

    v1 的写入请求体使用 file 指定路径:

    {
      "file": "/tmp/demo/README.md",
      "content": "# demo\n"
    }

    两者都用 JSON body。readwritereplacesearch 的路径字段是 file,其余路由用 path

    写入 body 的字段:

    字段取值含义
    content字符串,必填内容,按 encoding 解码后写入
    encodingutf-8(默认)、base64raw写入前如何解析 content
    append布尔追加而不是覆盖
    leading_newlinetrailing_newline布尔在内容前面或后面补一个换行
    • 写入会顺带创建缺失的父目录,返回 filebytes_written
    • base64 先把 content 解码成字节,raw 把每个字符当一个字节写。leading_newlinetrailing_newline 只在 utf-8 下生效。
    • 读取接口带缓冲,只支持文本:读目录返回 EISDIR,读非 UTF-8 文件返回 decode_error,读超过 16 MiB 的文件返回 file exceeds the 16 MiB buffered text limit。下载使用流式传输,不受此上限约束。

    文件与目录

    路由用途说明
    GET /v2/fs/stat单个路径的元信息follow_symlinks 决定看链接还是看目标
    GET /v2/fs/list列目录recursivemax_depthshow_hidden
    POST /v2/fs/edit带锚点的原地编辑str_replaceinsertundo_edit
    POST /v2/fs/mkdir创建目录parents 也接受已存在的目录
    POST /v2/fs/copy复制文件或目录树overwrite 才会覆盖目标
    POST /v2/fs/move移动或重命名body 与 copy 相同
    POST /v2/fs/delete删除路径非空目录需要 recursive

    编辑路由不包含 createview:两者都返回 400,并指出替代它们的路由 POST /v2/fs/writeGET /v2/fs/read

    路由用途说明
    POST /v1/file/stat单个路径的元信息follow_symlinks 决定看链接还是看目标
    POST /v1/file/list列目录多出 file_typesinclude_sizesort_by
    POST /v1/file/str_replace_editor带锚点的原地编辑viewcreatestr_replaceinsertundo_edit
    POST /v1/file/replace普通替换返回 replaced_count,没有编辑器语义
    POST /v1/file/mkdir创建目录parents 也接受已存在的目录
    POST /v1/file/copy复制文件或目录树overwrite 才会覆盖目标
    POST /v1/file/move移动或重命名body 与 copy 相同
    POST /v1/file/delete删除路径非空目录需要 recursive

    编辑器工具按锚点替换文本。调用时不需要上传整个文件,也不需要提供行号。

    请求需要这些字段:commandpathold_strnew_strinsert_linereplace_modeALLFIRSTLAST)。

    • old_str 必须逐字匹配,空白和换行符也必须一致。 找不到锚点时返回 400 old_str not found in file;匹配到多处时返回 400 old_str has multiple occurrences; replace_mode is required,不会自动选择其中一处。
    • data 里有 output(工具给模型看的 cat -n 片段)、old_contentnew_content(改动前后的完整文件)以及 prev_existundo_edit 会把上一次编辑替换掉的内容恢复回来。

    目录列表和 stat 的返回字段如下:

    • list 返回的每个 files 条目包含 namepathis_directorysizeextensionmodified_time
    • 列表外层包含 total_countfile_countdirectory_counttruncated。结果超过 10,000 条时会截断,并将 truncated 设为 true
    • is_hidden 只在值为 true 时返回;show_hidden 默认开启。permissions 只有在请求时才会返回。
    • stat 返回 pathsizepermissionsis_directoryis_symlinkmodified_time

    符号链接按指向的目标归类:指向目录的链接算目录,递归列目录时会列出该链接,但不会继续进入链接目标。

    follow_symlinks: false 会让 stat 描述链接本身。Windows 上的属性和权限见 Windows

    copymove 遇到已存在的目标会返回 already_exists,除非带上 overwrite,所以重命名不会悄悄覆盖东西。delete 要删非空目录必须带 recursive

    搜索

    按文件名找文件和在文件内容里搜是两类不同的路由。

    curl -X POST "$BASE_URL/v2/fs/grep" \
      -H "Content-Type: application/json" \
      -d '{"path": "/tmp/demo", "pattern": "argv", "recursive": true}'
    路由用途说明
    GET /v2/fs/search按文件名找文件pattern**/*.py 这样的 glob,返回路径列表
    POST /v2/fs/grep搜文件内容默认按正则,fixed_strings 按字面量
    curl -X POST "$BASE_URL/v1/file/grep" \
      -H "Content-Type: application/json" \
      -d '{"path": "/tmp/demo", "pattern": "argv", "recursive": true}'
    路由用途说明
    POST /v1/file/find按文件名找文件glob 是文件名模式,返回路径列表
    POST /v1/file/glob找文件并带元信息多出 sizemodified_timesort_byfiles_only
    POST /v1/file/grep搜一棵目录树的内容默认按正则,fixed_strings 按字面量
    POST /v1/file/search用正则搜单个文件返回 matchesline_numbers,行号从 0 开始

    grep 的请求体在 v1 和 v2 中相同:

    {
      "path": "/tmp/demo",
      "pattern": "argv",
      "recursive": true
    }

    grep 可以用 includeexcludetypemax_file_size 缩小搜索范围。

    case_insensitivemultilinecontext_beforecontext_aftermax_resultsoffset 控制匹配方式及返回内容。

    每条匹配包含 file、从 1 开始的 line_numberline_content。请求上下文时,结果还会包含相邻行。

    每种搜索都有结果上限,超过上限时会将 truncated 设为 true

    • grep 最多返回 500 条匹配,并跳过超过 1 MiB 的文件。
    • 带元信息的 glob 最多返回 5,000 条结果。
    • 按名称搜索最多返回 10,000 条结果。

    如果路由支持,可以增大 max_results,也可以缩小搜索根目录。

    文件传输

    • 上传:POST /v2/fs/upload
    • 下载:GET /v2/fs/download?path=...
    • 上传:POST /v1/file/upload
    • 下载:GET /v1/file/download?path=...
    路由用途说明
    uploadmultipart/form-data 传入一个文件返回 file_pathfile_sizesuccess
    download流式取出一个文件返回原始字节,不是统一返回结构
    HEAD download只取下载的响应头获取大小和校验字段,不传 body
    • 上传内容直接写入磁盘,不在内存中缓冲,因此文件大小受磁盘容量而不是内存限制。内容先写入目标旁边的 .aiod-upload-<id>.part,再重命名为目标文件。
    • 下载流式返回字节,带 Accept-RangesETagLast-Modified,支持 Range 请求头并返回 206,路径不存在返回 404
    • 默认情况下,下载返回的是流式读取过程中内核读到的内容;change_policy=abort 会在打开时固定文件状态,文件此前已变化返回 409,传输过程中变化则中断传输。

    需要把整个目录当成一个 tar 流搬运时,切到 v2:

    目录传输使用 tar 流:

    • GET /v2/fs/tree?path=... 将目录导出为 tar 流。
    • PUT /v2/fs/tree?path=... 接收 tar 请求体,并将内容写入目标目录。
    • 服务在进程内解包,不调用 tar 二进制。归档中包含 .. 或绝对路径的成员会被拒绝;归档中的属主和权限不会保留。

    响应中的 mode 表示写入方式:

    mode含义
    rename目标目录原本不存在,服务会把暂存目录整体移到目标路径
    merge目标目录已经存在,归档中的条目会逐个覆盖进去

    还有以下限制:tar 请求体不能超过 4 GiB;解包后的总大小不能超过 4 GiB;归档中的条目不能超过 100,000 个。

    监听文件变化

    要监听文件变化,请使用 watch,而不是反复读取目录。

    watch 只报告“内容可能已经变化”,不返回文件内容。收到事件后,请重新读取文件;如果本地有未保存的修改,则提示冲突。

    命令、其他 API 调用和构建过程对这棵目录树的修改都会产生事件:

    Python
    TypeScript
    from agent_sandbox import Sandbox
    
    client = Sandbox(base_url="http://127.0.0.1:18091")
    watcher = client.file.watch_create(
        path="/tmp/demo", recursive=True, debounce=200
    )["data"]["watcher_id"]
    
    client.bash.exec(command="echo 'X = 1' > /tmp/demo/util.py")
    polled = client.file.watch_poll(watcher, cursor=0, timeout=10)["data"]
    for event in polled["events"]:
        print(event["seq"], event["type"], event["relative_path"])
    # 1 create util.py
    # 2 write util.py
    print(polled["cursor"], polled["overflow"])
    # 2 False
    client.file.watch_stop(watcher)

    监听器使用 /v2/watch 路由:

    • POST /v2/watch 创建监听器。
    • GET /v2/watch/{id}/poll 轮询事件,通过 query 参数传入 cursorlimittimeout
    • GET /v2/watch/{id}/events 通过 Server-Sent Events 推送事件。
    • DELETE /v2/watch/{id} 释放监听器。
    • GET /v2/watch 列出仍在运行的监听器。

    监听器使用 /v1/file/watch 路由:

    • POST /v1/file/watch 创建监听器。
    • POST /v1/file/watch/{id}/poll 轮询事件,在 body 中传入 cursorlimittimeout
    • GET /v1/file/watch/{id}/events 通过 Server-Sent Events 推送事件。
    • DELETE /v1/file/watch/{id} 释放监听器。
    • GET /v1/file/watch 列出仍在运行的监听器。
    • POST /v1/file/watch/wait 阻塞等待指定路径发生变化。它接收 pathtimeout(默认 30 秒)和 event_types,返回单个 event;超时且没有事件时返回 503 timed out waiting for file event

    各项参数和取值范围:

    字段取值含义
    recursive布尔,默认开监听整棵子树
    debounce50–5000 毫秒,默认 300变更合并的时间窗口
    excludeglob 数组替换默认列表,不是在它基础上追加
    include_patternsglob 数组只报告匹配到的路径
    limit1–1000,默认 100单次 poll 返回的事件数
    timeout0–60 秒,默认 0一次 poll 等待首个事件的时长

    每个事件都包含以下字段:

    • 基本信息:seqtypecreatewriteremoverenamechmod)。
    • 路径信息:pathrelative_path
    • 文件属性:is_dirtimestampmtimesizeinode
    • rename 事件还会包含原路径 old_path

    cursor 表示客户端已经消费到的位置。每次 poll 时,把响应中的 cursor 原样传给下一次请求。

    如果缓冲区在客户端读取前丢弃了事件,响应会包含 overflow: truecursor: 0 会重放缓冲区中的历史事件;如果只想接收创建监听器之后的事件,应使用创建响应中的 initial_cursor

    过滤和写入行为:

    • exclude 默认排除 .gitnode_modules__pycache__.venv.DS_Store,以及常见的字节码和编辑器临时文件。传入 exclude 后会替换默认列表,不会在其基础上追加。
    • 不要排除 *.part:上传时会先为临时文件名报告 3 个事件,最后才在目标文件上报告 rename
    • 如果写入请求指定了属主,文件会先写入 .aiod-write-<id>.part,再进入目标路径。普通写入则直接使用目标文件的 inode,不会创建临时文件。

    使用完全相同的配置创建监听器时,daemon 会复用已有监听器。第二次创建会返回 reused: true 和当前的 initial_cursor

    每次 DELETE 只释放一个订阅者。最多可同时运行 128 个监听器;每个监听器保留最近 10,000 条事件。达到上限时,创建请求返回 429

    浏览器界面如果不想轮询,可以通过 Accept: text/event-stream 请求事件流。

    文件归属

    文件 API 始终使用 daemon 账户的权限读写文件。

    user 只决定新建的文件和目录归谁所有;command 和 terminal 中的 user 决定进程以哪个账户运行。

    支持创建文件或目录的 /v2/fs 请求都可以加上 ?user=alice 指定属主,包括 uploadPUT /v2/fs/tree

    例如,创建一个属于 alice 的文件:

    {
      "path": "/tmp/report.txt",
      "content": "report\n"
    }

    请求地址为 POST /v2/fs/write?user=alice

    v1 用 sudo: true 指定 root 账户。只有 readwritereplacesearch 四个路由支持该参数。

    需要让文件属于某个具名账户而不是 root 时,切到 v2:

    其他规则如下:

    • 不传 user 时使用默认属主。未设置 AIO_DEFAULT_USER 时,默认属主就是 daemon 自己的账户。
    • 账户不存在时返回 400 no such user: alice
    • 非 root 的 daemon 无法切换属主,会返回 400 cannot run as alice: aiod is running as uid 501 and only root can change identity,不会静默使用错误的属主写入。
    • 仅限 Linux。

    agent 使用建议

    大多数文件操作都可以按下面的流程完成:定位文件,读取需要的部分,原地编辑,再交给其他 plane 处理。

    写入文件后再执行命令需要两次调用,因为两个 plane 共享同一个文件系统。

    任务调用然后
    修掉项目里的一处 TODOgrep 找到那段文本按锚点编辑,再读取对应行
    读一个大文件stat 看大小只读一段行范围,不要整个文件读
    把数据交给命令写文件执行命令,它可以立即读取文件
    收集构建产物监听输出目录从 cursor 往后 poll,下载新出现的文件
    整体搬入或搬出项目PUT/GET /v2/fs/tree一个 tar 流,而不是一个文件一个文件传

    文件操作 完整演示了这一流程:创建项目、找到 TODO、修改它、在命令修改目录时监听变更,最后传出结果。

    错误处理

    可预期的文件系统失败在两套接口上返回同样的结构化 data,区别在于外层的 HTTP 状态码。

    状态码表示失败的类别,data.error_type 给出具体名字:

    error_type状态码场景
    not_found404路径不存在
    permission_denied403daemon 账户无法访问目标路径
    already_exists409复制或移动到已存在的目标
    bad_requestinvalid_pathinvalid_target400模式非法,或者把目录当文件读
    decode_error422把非 UTF-8 文件当文本读
    no_space_left507文件系统满了
    其余情况500没有更合适映射的操作系统错误

    请求体或 query 字段格式不对是 422,带 errors 列表,其中 locationbodyquery 开头。

    可预期的文件系统失败一律返回 HTTP 200success: false

    判断失败类型时,先看 success,再看 data.error_type。唯一例外是请求体或 query 字段格式错误,此时返回 422errors 列表。

    data 描述这次失败:

    • errnoerrno_name —— 操作系统错误码和它的符号名,比如 ENOENT
    • error_type —— 失败的类别:not_foundpermission_deniedalready_exists……
    • exception_type —— 对应的异常类名,比如 FileNotFoundError
    • messageoperationpath —— 操作系统给的消息、失败的操作、涉及的路径
    • retryable —— 重试是否可能成功

    download 是两个版本中唯一直接返回字节流的文件路由;路径不存在时返回 404

    watch 路由仍使用统一返回结构,但监听器生命周期相关的失败使用 HTTP 状态码表示:未知监听器为 404debouncelimit 越界为 400,达到监听器上限为 429wait 超时为 503

    相关页面