Files
Eta_change/docs/AGENT_RUNTIME.md
DelLevin-Home 8a0e5e4bd3
Some checks failed
Eta Build / 构建 Debug 与 Release APK (push) Has been cancelled
修改github eta 2.6.1 兼容android13 去掉超级小爱hook
2026-08-18 20:36:17 +08:00

15 KiB
Raw Blame History

Agent Runtime

Eta 的 Agent Runtime 负责把一次用户输入组织为模型回合、工具执行和可持久化的增量 transcript。它运行在模块自身进程Hook 进程只负责识别入口、发送请求和接收结果。

代码边界

  • AgentModelClient:稳定门面、配置与跨进程会话 DTO。
  • AgentLoop:单次 run 的状态机,不依赖 Android Service、Room 或 Compose。
  • AgentPromptBuilder系统约束、Skill 索引、历史和当前用户输入。
  • AgentConversationCodecProvider JSON 与稳定会话 DTO 的转换。
  • AgentToolCatalog 及分组目录:模型可见的工具 schema不执行工具。
  • AgentTraceFormatter:只生成可展示、可记录的脱敏摘要。
  • AgentProviderClientOpenAI-compatible、Anthropic 等协议边界。
  • AgentRunController:取消、暂停和 steering 队列。
  • AgentRuntimeSession:每个 run 自持 reply channel并保证唯一最终结果。
  • AgentRuntimeRunExecutor:从 Skill/工具初始化到模型执行、资源清理和终态提交的统一异常边界。
  • AgentRuntimeServiceAndroid 生命周期、入口 IPC 和浮层宿主;不再内联 Agent 执行循环。
  • ShellProcessSupervisorAndroid/Alpine Shell 进程的接纳、独立进程组、取消和回收;终端协议不承担进程所有权细节。

Loop 语义

一个 turn 是“一次 assistant 响应 + 该响应提交的完整工具批次”。循环遵守以下顺序:

pending steering
→ provider response
→ assistant history
→ tool batch按模型顺序串行执行
→ contiguous tool results
→ optional image observations
→ next turn / final result

关键不变量:

  • steering 默认逐条排队,只在当前 turn 完整结束后注入;它不会取消当前 HTTP 请求或关闭工具资源。
  • 同一 assistant 消息中的全部 tool result 必须连续写入,再追加不受 Provider 原生 tool-result image 支持的图片观察。
  • finish_reason=lengthmax_tokens 且包含工具调用时,不执行任何可能被截断的参数;为每个调用写入结构化错误结果,让模型重新规划。
  • 只有明确的 tool_calls / tool_use 终止原因才允许执行工具;stop、内容过滤或未知终止原因中夹带的调用一律作为协议矛盾拒绝。
  • 工具参数在执行前按本轮实际下发的 JSON Schema 重新校验。模型输出不是可信输入,缺少坐标等必填字段时不得调用设备执行器。
  • transcript 只返回本次 run 新增的 assistant、tool 和运行中 steering 消息,不重复旧 history 或本轮初始用户消息。
  • GUI/终端工具保持串行。Android 前台状态和会话式 Shell 都不具备可安全并行的通用语义。
  • 单次 run 当前最多 64 个模型回合、256 个工具调用,防止异常模型形成无界循环。
  • 最后一个允许回合不会再启动工具副作用,因为其结果已经没有下一回合可以消费。
  • cancel 是终止信号pause 是检查点阻塞steering 是下一回合输入。三者不能互相模拟。
  • cancel 的主线程路径只做原子终态与资源关闭:共享浏览器按 runId 校验归属;终端立即封闭新的进程接纳,并在后台按独立进程组终止同步命令、会话和 async job再完成线程与流回收。Android 上 setsid 或 PID/PGID ownership 握手不可用时会 fail closed非 Android 测试环境才允许父子树快照回退。终止前还会核验随机 ownership token避免陈旧 PGID 复用后误杀无关进程。
  • 最终 steering 检查会原子关闭接收入口Loop 返回后不会再把无人消费的补充指令误报为已接收。补充指令也不会解除 pause。
  • 新 run 替换旧 run、用户取消和正常完成都通过 AgentRuntimeSessionRUNNING → COMMITTING → TERMINAL 状态机竞争唯一终态;提交胜者独占 outbox、归档和最终发布客户端另有 30 分钟兜底超时。
  • 入口请求只能缩小工具能力不能自行授权。Runtime 在开始 run 时裁剪配置,在每次浏览器、终端和设备工具执行前重新读取用户开关,并在 thinking 关闭时移除自定义请求体中的 reasoning/thinking 覆盖字段。
  • 设备工具分为直达工具、敏感读取工具和敏感操作工具当前均默认开启。Runtime 在每次执行前重新读取用户开关;开关允许且参数通过 schema 与执行器校验后即可执行,不再用固定关键词二次匹配用户原话。
  • 微信发送不提供专用工具、参数协议或额外策略层,完全使用通用 GUI 工具观察和操作微信界面。
  • 通知、短信验证码、WiFi 凭据和日志属于瞬时敏感工具数据。当前模型回合可以使用原始值,但持久 transcript 会同时替换对应工具参数和结果,避免进入会话数据库或后续 IPC。

Provider 协议

OpenAI-compatible Provider 可在配置页选择 Chat CompletionsResponses API。新安装和重置后的内置 OpenAI 默认使用 Responses数据库中已有 Provider 不会被默认值覆盖。自定义 Provider 和其他内置 Provider 默认仍使用 Chat Completions。

Responses 请求固定使用 stream:truestore:false,不发送 previous_response_id。Runtime 把本地历史转换为 typed input Items并在同一次 run 的工具回合之间精确回放 Provider 返回的完整 output Items因此 encrypted reasoning、服务端工具状态等 opaque 数据只存在于内存,不进入 IPC transcript、Room、日志或运行归档。持久会话只保留规范化回答、可见推理内容和 Eta 工具记录,后续 run 由这些稳定数据重新构建上下文。

兼容接口若在 response.completed 中省略 output 或返回空数组Runtime 只使用同一 SSE 流中已经收到的标准文本、推理摘要和函数调用增量完成当前轮次;非空终态始终是权威结果,且本地恢复结果不会冒充 Provider 的 opaque output Items。

推理界面展示的是 Provider 返回的 reasoning summary它不是原始思维链也不会由 Eta 伪造。兼容 Provider 若按 Responses 协议返回 reasoning_textRuntime 会把它作为可见推理内容展示。Responses 只对精确命中官方目录且未被远端显式标记为 reasoning:false 的模型补齐推理能力,不会因 Endpoint 类型而假定所有模型支持推理。

服务端网页搜索是 Responses Provider 的独立开关,默认关闭。开启后请求只增加 web_search 托管工具;搜索开始和结束作为独立运行事件投影到 UI不进入 Eta 本地工具执行器。最终回答中的 url_citation 会去重并转换为可点击 Markdown 引用;偏移无效时降级为回答末尾的来源列表。当前不接入 file search、code interpreter、MCP 或其他托管工具。

长期记忆

长期记忆保存在 App 私有目录的单一 MEMORY.md 中。文件使用 UTF-8安全上限为 1 MiB仓库在进程内锁中应用变更并通过 AtomicFile 覆盖完整文件。模型写入携带当前内容的 SHA-256 revisionrevision 不一致时返回 MEMORY_CONFLICT,不会覆盖并发更新。

每次 run 只把 # 核心记忆 的预算内内容、一级/二级标题索引和 revision 放入系统背景。核心预算为 min(32000, max(4000, contextWindow / 16)) 个字符;模型窗口未知时按 128K 计算。没有 # 核心记忆 标题时不自动注入正文。其余内容由 memory_get 按行分页或按文本检索,单次最多返回 32000 字符。

memory_write 支持行区间替换、独立章节追加与清空;单次模型生成内容最多 3500 字符,设置页的用户手动编辑不受此单次工具限制。关闭记忆不会删除文件,后续 run 不再注入或暴露工具;已开始的 run 在每次执行记忆工具前也会重新检查开关。

记忆内容只作为可编辑背景,不具有指令优先级。记忆工具原始参数与结果可供当前 Agent Loop 使用,但对应工具调用在持久 transcript 中整体脱敏;运行事件只保存操作类型、行数、字节数和错误码,不保存正文或查询词。

终端环境

terminalenvironment 明确区分设备控制与通用 Linux 工具,默认值为 android

  • android 继续使用系统 Shell。user 身份不升级权限;root 身份在 su 内探测 Magisk、KernelSU、APatch 或系统 BusyBox并优先进入 standalone ash,因此 BusyBox applet 不要求预先加入 PATH。旧 run_command、文件读写和目录操作保持这一环境,避免改变既有 Android 路径与命令语义。
  • linux 仅允许 root,并要求用户先在设置中安装 Eta 管理的 Alpine 环境。每个命令或会话进入独立 mount namespace挂载必要的 /proc/dev 以及可用的共享存储后再 chroot/workspace 绑定 Eta 的 Android 工作目录 /data/local/tmp/fuck_andes,并作为 Linux 默认工作目录,/sdcard 继续指向共享存储。进程结束时命名空间一并销毁,不把 bind mount 留在 Android 全局。chroot 只提供 Linux userland不构成安全沙箱。

安装器只接受代码中固定版本、大小和 SHA-256 的 Alpine 官方 minirootfs先在临时目录校验并解压再原子替换 App 私有 rootfs。默认工具档案包含 Agent 高频使用的搜索、差异、补丁、Git/SSH、传输、结构化数据、进程与压缩工具以及 Python、pip、venv、pipx、uv 和 Ruff。工具档案使用版本化完成标记旧安装会保留 rootfs 和用户文件,只补装当前档案。工具安装完成前不会写入完成标记,失败后可继续安装。

APK 分析是用户主动安装的独立档案。JADX、Apktool、smali 与 baksmali 使用固定官方 Release URL、大小和 SHA-256下载完整校验后才进入 App 可写的 cache staging不能把下载或解包暂存目录放进由 Root 创建的 Alpine 管理目录。GitHub 实际制品域名不可达时,安装器按固定顺序尝试 HTTPS 下载代理,但仍只接受与官方清单 SHA-256 完全一致的字节。JADX 只解出 CLI 脚本、运行库与许可证,成功验证全部命令后再原子切换当前版本。档案安装 OpenJDK 17但不安装全局 Gradle、Android SDK 或 NDK。由于官方与 Apktool 随附的 Linux AAPT2 都不能直接用于手机 arm64 环境,apktool build 在当前档案中稳定拒绝;解码、代码查看和独立 Smali 汇编/反汇编不受影响。

上下文与续接

App 在发起请求前已经把当前用户消息写入会话 history因此 Runtime 返回的 transcript 必须保持“增量”语义。已完成 run 的补充请求由 AgentContinuationBuilder 使用以下顺序重建上下文:

旧 history
→ 原始用户消息
→ 完整增量 transcript
→ 新补充消息

图片只在需要它的当前模型回合中传递;持久 transcript 会删除 data URL并写入稳定的省略说明避免截图 base64 同时膨胀 Binder、Room 和后续上下文。外部入口归档可以另外保存有界的小预览用于还原用户消息 UI但预览不会重新进入模型历史。启动请求在发送前按实际 Parcel 大小校验,超过 768 KiB 时会明确拒绝并提示减少图片数量或分辨率。运行归档 transcript 上限为 100 万字符;会话上下文检查点和直接 IPC transcript 上限为 9.6 万字符outbox 批量 drain 使用更紧的单项预算,确保最坏 8 条待交付结果仍处于 Binder 事务预算内。任何容量压缩都会在保留的 history 前插入明确的 Eta system notice不会把删头后的 transcript 冒充成完整上下文。会话元数据、逐条展示消息和有界上下文检查点分别存储;会话列表查询不读取上下文正文,启动时也不会因单个长期会话阻塞全部会话恢复。

浮层在已完成结果后发起的 continuation 会在 handoff 中只携带本次新增的 prompt supplement不累计复制旧补充。App 回到前台时 drain outbox把该用户消息和增量 transcript 一起写回 history。

自动重试、上下文压缩和跨 run、跨 Provider 的 opaque reasoning 状态尚未由当前 Loop 冒充实现Responses output Items 只在当前 run 内回放,不能作为持久会话状态。

Skills 安装边界

Skill 安装工具始终向模型提供,不再根据顶层用户输入的固定关键词决定是否暴露或执行。网页、仓库 README 和已安装 Skill 仍只是数据,不能改变工具参数或执行边界。

  • AI 安装只访问公开 GitHub HTTPS 地址curated 默认来自 openai/skillsskills/.curated。安装路径必须来自当前 run 对同一仓库与 ref 的检查结果,最多 20 个。
  • 本地 ZIP 由 Skills 页面通过系统文件选择器读取,不申请共享存储权限,也不把归档复制到公开目录;每个 ZIP 只允许包含一个 Skill。
  • GitHub 下载与本地 ZIP 共用受限解包和校验流程:拒绝路径穿越、绝对路径、重复条目、嵌套 Skill、非法 frontmatter以及超过条目数、单文件、归档或总解压预算的输入。
  • 安装先在 App 私有临时目录完整验证,再提交到正式 Skills 目录。文件系统与 Room 变更由持久事务日志协调,进程异常退出后会在下次变更前恢复;批量安装任一步失败都会回滚。同名用户 Skill 默认保持不变GitHub 单冲突替换绑定仓库、提交、路径和 Skill ID可在同一 run 精确重试;内置 Skill 永远不能被导入包覆盖。
  • 安装只保存文件、登记索引并默认启用,不执行 scripts/,也不改变终端/文件工具开关。本轮 Skill 索引在模型调用前已经冻结,因此新 Skill 从下一轮对话开始可用。

已安装 Skill 的附属文本资源通过独立的有界读取工具访问读取时再次做相对路径、canonical root、UTF-8 与大小检查;脚本和二进制 asset 不会借此被执行或当作无限文本送入上下文。

待确认结果和外部入口归档会把 transcript 一并写入 Room。数据库 6 → 7 使用显式非破坏迁移为旧记录补 transcript7 → 8 为会话增加已应用 run 标记10 → 11 将会话上下文迁入独立的有界检查点并清理旧的大字段。恢复幂等性不再靠比较 history 尾部猜测;保存任务严格按调用顺序串行,只有包含对应标记的快照落盘后才 ACK outbox。旧 6.x 结果仍可用已有 assistant 内容合成兼容 history。

验证

核心回归测试位于:

  • AgentModelClientLoopTest
  • AgentConversationCodecTest
  • AgentRunControllerTest
  • AgentContinuationBuilderTest
  • AgentRuntimePolicyTest
  • AgentRuntimeSessionTest
  • AgentToolCatalogTest
  • AgentMemoryStoreTest
  • AgentMemoryContextBuilderTest
  • FuckAndesDatabaseMigrationTest

最终验证仍运行项目统一命令:

./gradlew :app:assembleDebug :app:testDebugUnitTest :app:lintDebug