pi.provider / baseUrl / model / thinkingLevel / apiKeySecretRef are now DEPRECATED_SETTINGS_FIELDS: kept in the schema for stored JSON, dropped from PATCH /settings and from the settings sections, never read by the server. runAgent fails with a clear message when no upstream is set. The agent demo gate checks the default upstream provider; e2e seeds a fake upstream as default. OpenAPI summaries say Agent upstream. Co-Authored-By: Claude Opus 5.5 <[email protected]>
259 lines
26 KiB
Markdown
259 lines
26 KiB
Markdown
# Agent 运行手册(W2-D)
|
|
|
|
实现:`apps/server/src/modules/agent/`(会话 API、runAgent / resolveDecision 任务、授权与账本)、
|
|
`apps/server/src/modules/acquisition-organize/capabilities.ts`、`apps/server/src/modules/sources-identity/capabilities.ts`、
|
|
`packages/agent`(Pi 接入、FakeModelProvider)、迁移 `0021_agent_runtime`。
|
|
|
|
## 能力
|
|
|
|
启动时装配 15 个能力(`catalog.lookup`、`release.search`、`release.inspect`、`identity.resolve`、`download.plan`、`download.submit`、`download.recheck`、`organize.plan`、`organize.execute`、`task.inspect`、`task.retry`、`task.cancel`、`policy.preview`、`policy.apply`、`inventory.query`)。注册数不等于 `CAPABILITY_IDS.length` 时进程启动失败。Web(`POST /capabilities/{id}/invoke`)、Pi 工具和后台任务都走 `ctx.capabilities.invoke` 这一条路径。
|
|
|
|
- **授权**(`agent/authorize.ts`):propose / approve / execute 类必须满足以下之一:
|
|
- 单次审批 `approval`:状态为 approved,绑定计划类型 + 计划 ID + payload hash + revision,且未过期。执行成功后审批标记为 consumed。
|
|
- 持续授权 `automation_grant`:`action` 覆盖该能力(比如 `download.submit`,或策略发布自动生成的 `policy.acquisition`,或 `*`),状态 active 且未过期,绑定的策略版本仍是 published。`scope.mediaEntityIds`、`scope.mediaKinds`、`scope.capabilities` 要包含目标;`budget.maxInvocationsPerDay`、`budget.maxBytesPerDay` 还有余量。
|
|
|
|
模型参数里写"用户已同意"不起作用。输入里的 `approvalId` 只是一个指针,授权结果以库里的审批记录为准。控制台管理员从 Web 手动调用(actor=`user:*`)算人工授权,API Key、Agent 和后台任务都必须有授权记录。自动化总开关关闭或处于只读对账时,自动来源的 execute 一律拒绝。
|
|
- **审计**:每次调用都写一条 `audit_event`,action 为 `capability.invoke` 或 `capability.denied`,payload 包含 outcome、授权 ID、幂等键和脱敏后的输入摘要。
|
|
- **幂等键与操作账本**:有副作用的调用由服务端生成幂等键,格式为 `id:v版本:<agentRunId|taskId>:sha256(规范化输入)`,并记入 `capability_invocation`。状态为 succeeded 时直接返回记录的结果。状态为 running(进程在调用途中崩溃)时,用同一个键重跑处理器来对账,处理器本身是幂等的。
|
|
- `download.plan` 只生成 **draft** 计划:不预留 Slot,不计入预算,也不会被后台巡检补提交。`download.submit` 在授权通过后先把 draft 转成 reserved(Slot revision 必须与生成计划时一致),再入队 `submitDownload`,和自动路径共用幂等键 `plan:<planId>`。
|
|
- `organize.plan` 把冻结的整理计划存成 artifact(kind=plan),`payloadHash` 就是计划指纹。`organize.execute` 执行前会重新冻结一次,指纹不一致就返回 conflict。
|
|
|
|
## 会话 API
|
|
|
|
| 端点 | 说明 |
|
|
|---|---|
|
|
| `POST /agent/runs {goal, mode: execute\|plan_only, capabilityAllowlist?}` | 建 `agent_run` 和业务任务行(kind=agentRun),并入队持久任务 `runAgent`。**HTTP 请求里不跑模型**,返回时状态为 `queued`。`plan_only` 只允许 read / preview / propose 类能力,白名单里放了 execute 类会返回 422 |
|
|
| `GET /agent/runs` / `GET /agent/runs/{id}?afterSeq&limit` | 详情包括:`agent_run_event` 按 seq 分页、追加的消息、服务端核查过的目标汇报(`report`) |
|
|
| `POST /agent/runs/{id}/messages {text}` | 追加用户消息。运行中时,当前会话在本轮结束后消费;已结束或等待中时,会唤醒一次新的 runAgent 续跑 |
|
|
| `POST /agent/runs/{id}/abort {reason?}` | 写取消标志,会话监视器会中止模型并收尾为 cancelled。只停止后续步骤,不撤销已经完成的外部动作 |
|
|
| `GET/POST /automation-grants`、`PATCH /automation-grants/{id}` | 持续授权。PATCH 只能撤销或缩短有效期,扩大范围要新建一条 |
|
|
| `POST /approvals {planType, planId}`、`PATCH /approvals/{id} {status, payloadHash}` | 单次审批。**只接受控制台管理员会话**(API Key 一律 403,持 `organize.execute` 的 Key 不能给自己的计划开审批再自批)。批准时要回传 payloadHash;计划 payload hash / revision 变化、过期、撤销后旧审批失效,执行成功一次即 consumed,不可重放(`test/agent/approvals.test.ts`) |
|
|
| `GET /capabilities`、`POST /capabilities/{id}/invoke` | 能力目录与调用,HTTP scope 按能力描述符再校验一次 |
|
|
| `GET /tasks[/{id}]`、`POST /tasks/{id}/cancel`、`POST /tasks/{id}/retry` | 业务任务;`?subjectId=` 按业务对象查(submitDownload 的 subject 是计划,verifyAndImport 的是下载任务) |
|
|
| `POST /acquisition-plans {sourceReleaseId, slotId? \| entityId?, note?}` | 手动提交下载(工作台下载入口):依次调 `download.plan` → `download.submit`,和 Agent 同一条能力路径(授权 / 审计 / 幂等键 / 账本)。没给 slotId 时按作品的 Slot 创建顺序逐个尝试,第一个生成计划的胜出;都没生成时 `submitted=false` 并逐个 Slot 给出原因。控制台管理员算人工授权;API Key 需要持续授权或单次审批 |
|
|
| `GET /artifacts[/{id}]` | 产物只读:整理 dryRun 计划(kind=plan)、目标汇报(report)、候选评估(evidence)等 |
|
|
|
|
**增量推送**:事件先写 `agent_run_event`(run_id + seq 唯一),再经 `ctx.events` 推到全局 SSE(`GET /events`)。推送的类型有 `agent.run_status`、`agent.text_delta`、`agent.tool_call`;未授权导致的工具失败,`phase` 为 `denied`。断线后,先用全局 `?after=<seq>` 补读总线事件,再用 `GET /agent/runs/{id}?afterSeq=<最后收到的 runSeq>` 补读单个 run 的完整事件。SSE 只给控制台(cookie 会话);API Key 调用者用 `GET /events?after=<seq>&limit=<n>` 取 JSON 页轮询(见 auth.md「SSE 多进程总线」)。
|
|
|
|
控制台 Agent 页直接用这些端点(合同取 `@umt/contracts` 的 `AgentRun` / `AgentRunDetail` / `AgentGoalReport`),`?mock=1` 只是演示开关(本地假数据,不连后端,`?mock=0` 退出),只在开发构建或后端默认上游 provider 为 `fake` 时生效;生产构建连真实模型时忽略并清掉记住的开关。页面收到 `agent.*` 事件后按 `afterSeq` 增量补读,`agent.run_status` 到达即刷新 run 快照;SSE 断开时 2.5 秒轮询兜底。
|
|
|
|
## runAgent 做什么
|
|
|
|
1. `drive` 步骤:
|
|
- 取上游(provider / baseUrl / 模型 / 思考档位来自 `pi_upstream`,凭证由上游的凭证存储在请求时解密注入,不落日志),从设置读上下文预算、压缩和预算。
|
|
- 检查今日次数和费用预算(`budget.agentMaxRunsPerDay` / `agentMaxCostUsdPerDay`)。
|
|
- 打开 `AGENT_SESSION_DIR/<runId>.jsonl`。`pendingOperations>0` 说明上次崩溃:先按幂等键对账,再 `resume()`;否则就 `prompt(goal)`。最后逐条消费追加的消息。
|
|
2. `goal-settled` 步骤:只在 execute 模式且会话正常结束时执行。每隔 `agent.goalPollSec` 检查一次,直到本 run 触达的计划、下载和整理记录全部落定,最长等 `agent.goalWaitHours`。等待期间 run 状态为 `waiting`,汇报写的是"已提交≠已入库"。
|
|
3. `finalize` 步骤:从业务表汇总计划、下载任务和整理记录,生成 `AgentGoalReport`(artifact kind=report),并据此决定终态:
|
|
- 入库完成 → `succeeded`,汇报带整理记录 ID;
|
|
- 仍在等待 → `waiting`;
|
|
- 失败 → `failed`;
|
|
- 超预算 → `budget_exceeded`,同时开一条待处理事项 `agent_budget:<runId>`;
|
|
- 取消 → `cancelled`。
|
|
|
|
模型的最终文本只附在汇报后面,标注"模型汇报(未经核查)"。
|
|
|
|
worker 崩溃后,新 worker 在引擎租约到期后(约 30s)接手,`drive` 步骤会从头重跑。已经完成的工具调用不会重放。副作用调用的结果模型会看到"结果未知",模型需要重新核对;同参数再调用一次,会直接拿到账本里的结果。
|
|
|
|
## 后台触发与 resolveDecision
|
|
|
|
- `decision.created` 路由到 `resolveDecision(trigger=created)`,由 `agent/investigate.ts` 的 `autoInvestigate` 处理。满足以下全部条件时,会发起一次有界 run:`agent.autoInvestigate=true`、kind 为 `identity_ambiguous` / `download_failed` / `download_stalled`、事项仍为 open、事项不是由 Agent 自己引起的。`trigger=event`,工具调用上限为 `agent.investigateMaxToolCalls`,时长上限 `min(600, agent.maxDurationSec)` 秒,`dedupe_key=investigate:<decisionId>`。同一事项只会有一个 run,重复事件或重新打开都不会再建。模式见下一节。调查结束后,没有被系统核实恢复的事项转为 `awaiting_user`,`evidence.agentInvestigation` 里带上结论。
|
|
- `decision.updated(status=resolved)` 路由到 `resolveDecision(trigger=updated)`,执行用户选中的处理:
|
|
- 选项带 `capabilityId` 时经注册中心调用(照样校验授权);
|
|
- `retry` 重试关联任务;
|
|
- `reevaluate` / `accept` 按对象重新发起 evaluateAcquisition;
|
|
- `dismiss` / `ignore` 不做任何动作。
|
|
|
|
执行结果写进 `decision.resolution.execution`。身份歧义类由 sources-identity 在处理端点里直接执行。
|
|
|
|
## 自动调查与恢复
|
|
|
|
依据 REQ §8.4.2(下载无进展默认不换源)、§10.7(有界任务、同根因去重、有限重试)、AT68 / AT71。
|
|
|
|
**模式**(`agent.autoInvestigateMode`):
|
|
|
|
| 模式 | run 参数 | 能做什么 |
|
|
|---|---|---|
|
|
| `investigate`(缺省) | `mode=plan_only`,白名单 = 只读能力 + `identity.resolve` | 查证据、给结论与建议,事项交还人工 |
|
|
| `recover` | `mode=execute`,白名单 = 上面 + `task.retry` + `download.recheck`,`created_by=system:auto-recover` | 在持续授权范围内执行恢复动作,系统核实后自动关闭事项 |
|
|
|
|
**恢复范围**:
|
|
- `download.recheck`(execute,replay=never):对已纳管、未移除的下载任务调 qB 强制重新校验(`action=recheck`,起点 missing / failed / stalled / paused),或恢复暂停(`action=resume`)。缓存阈值暂停和人工暂停不自动恢复。只动这一个种子,不换源、不删除、不改路径 / 分类 / 标签。
|
|
- `task.retry`:重试失败 / 死信的业务任务,副作用由各处理器的操作账本去重。
|
|
- `identity.resolve`:按已有证据等级自动接受;证据不够时仍进待处理。
|
|
|
|
**明确排除**:换源(`download.plan` / `download.submit`)、删除与缓存清理、`task.cancel`、`organize.execute`、`policy.apply`。这些能力不在恢复白名单里,模型调用会得到 `tool_call_rejected`。
|
|
|
|
**授权**:每次恢复调用仍经 `authorize` 校验持续授权,模型声明无效。可用的 grant:
|
|
- `action=download.recheck`,或发布 kind=automation 的策略时自动生成的 `policy.automation`;
|
|
- `action=task.retry` / `task.*`;`identity.resolve` / `policy.identity`;或 `*`。
|
|
|
|
grant 必须 active、未过期,绑定策略版本时该版本必须仍是 published。`scope.mediaKinds` / `scope.mediaEntityIds` 由 `download.recheck` 处理器按下载任务 → 计划 → Slot → 作品核对。注意:authorize 对非计划类能力拿不到目标作品,所以带 `mediaEntityIds` 的 grant 目前一律不覆盖 `download.recheck`,是保守拒绝。建 run 前先查有没有覆盖该类事项恢复动作的 grant(下载类看 `download.recheck` / `task.retry`,身份类看 `identity.resolve`)。没有就**降级为 investigate**,并在事项上写 `evidence.autoRecover.degraded` 和 `suggestion`,再追加一个可直接点选的"强制重新校验"选项(`capabilityId=download.recheck`,人工点选同样经授权、审计、账本)。
|
|
|
|
授权示例(把恢复限定在某个已发布策略下):
|
|
|
|
```sh
|
|
curl -X POST localhost:4380/api/v1/automation-grants -H "Authorization: Bearer <admin>" -H 'content-type: application/json' \
|
|
-d '{"action":"download.recheck","policyRevisionId":"<published automation 策略版本>","reason":"下载异常自动重新校验"}'
|
|
```
|
|
|
|
**核实与收尾**(`runAgent` 里的 `recover-*` 步骤):
|
|
1. 从账本(`capability_invocation`)取出本 run 成功的恢复动作,从审计取出被拒绝的调用。
|
|
2. 按业务事实轮询核实,间隔 `min(goalPollSec, 30)` 秒,最长 `agent.recoverVerifySec`:
|
|
- `download_task` 进入 downloading / queued / completed / importing / imported 算恢复,removed 算失败;
|
|
- 重试的 `task` 为 succeeded 算恢复,failed / dead / cancelled 算失败;
|
|
- `source_release` 已 resolved 算恢复。
|
|
3. 核实已恢复:事项转 `resolved`,`resolved_by=system:auto-recover`,`resolution.execution={kind:"agent_recover", verified:true, facts}`,发 `decision.updated`(executeResolution 看到 execution 已存在,不会重复执行)。run 汇总以"已恢复(系统核实)"开头,模型原话附在后面,标注"未经核查"。
|
|
4. 失败、超时(unverified)、预算超限、没有执行恢复动作:只写 `evidence.autoRecover.outcome` 和建议,事项由 finalize 转 `awaiting_user`,不会自动再试。run 汇总以"未恢复:…"开头。
|
|
|
|
**有界尝试**:`evidence.autoRecover.attempt` 记录该根因的第几次自动恢复。同一去重键(如 `download_failed:<downloadTaskId>`)下,历史上 `recoverAttempted=true` 的事项数达到 `agent.investigateMaxAttempts`(缺省 2)后,同根因的新事项不再唤醒模型,直接 `awaiting_user`,并写 `skipped=attempts_exhausted`。
|
|
|
|
## 上游与登录(W7-A)
|
|
|
|
实现:`apps/server/src/modules/pi/`(上游 CRUD、能力表、测试、服务端订阅登录、刷新任务)、`packages/agent/src/pi/{catalog,upstream,fake-upstream}.ts`、
|
|
合同 `packages/contracts/src/pi.ts`、迁移 `0027_pi_upstreams`。研究结论见 `docs/design/pi-providers.md`。
|
|
|
|
**模型**:一个上游(`pi_upstream`)= 一个 pi provider + 一份凭据。provider 是 pi-ai 0.87.1 内置的 41 个之一,或
|
|
`openai-compatible`(自定义兼容端点,baseUrl 与模型必填,`extra.api` 选 openai-completions / openai-responses / anthropic-messages / google-generative-ai),
|
|
或 `fake`(脚本化假模型,不联网)。同一 provider 可以建多个上游(两个 Codex 账号、两个 OpenAI key):运行时每个上游一个独立的 pi `Models`,
|
|
凭据存储只映射到本上游的 secret。认证方式 `authKind`:
|
|
|
|
| authKind | 凭据 | 说明 |
|
|
|---|---|---|
|
|
| `api_key` | secret 表存明文 key(`llm_api_key`),`extra.env` 存非密配置(Cloudflare ACCOUNT_ID、Azure 资源名等,见能力表 `envFields`) | 不读宿主环境变量,避免误用宿主的 key |
|
|
| `oauth` | secret 表存 pi 的 token 包 JSON(access / refresh / expires / accountId…) | 订阅登录,见下 |
|
|
| `none` | 不存密钥 | fake、无鉴权的本地端点、Bedrock 凭据链 / Vertex ADC(只有这一种会读进程环境变量与 `~/.aws`、gcloud 文件) |
|
|
|
|
默认上游是 `settings.pi.defaultUpstreamId`(只能经 `/pi/upstreams` 的 `makeDefault` 改;第一条上游自动成为默认)。
|
|
runAgent 按 `runAgent{upstreamId}` → `agent_run.upstream_id` → 默认上游选上游,都没有时 run 直接失败(「未配置 Agent 上游」)。旧字段 `pi.provider / baseUrl / model / thinkingLevel / apiKeySecretRef` 自 W8-A 起弃用:服务端不读、`PATCH /settings` 丢弃、设置页不展示(`DEPRECATED_SETTINGS_FIELDS`);e2e 直接建一条 `provider=fake` 的上游设为默认。
|
|
选中的上游写回 `agent_run.upstream_id / provider / model`。上游停用、未登录(`auth_required`)或登录失效(`expired`)时 run 直接失败并给出原因。
|
|
0027 迁移把已保存的旧 `settings.pi`(anthropic / openai-compatible / fake)转成一条上游并设为默认,旧 `apiKeySecretRef` 移交给上游(设置里清空)。
|
|
|
|
**端点**(全部 admin scope;写、测试、登录只接受控制台会话,API Key 403):
|
|
|
|
| 端点 | 说明 |
|
|
|---|---|
|
|
| `GET /pi/providers?models=false` | 能力表:authKinds(第一个是推荐项)、`oauth.modes`(browser / device)、`termsNote`(条款风险)、`baseUrlOverridable`、`envFields`、静态模型表 |
|
|
| `GET /pi/upstreams`、`GET /pi/upstreams/{id}` | 不含密钥,只给 `hasCredential`、`credentialMask`(前 4 后 4)、`account`、`oauthExpiresAt`、`isDefault` |
|
|
| `POST /pi/upstreams`、`PATCH /pi/upstreams/{id}`、`DELETE …` | `apiKey` 只写(PATCH 传 null 清除);provider 不可改;改 authKind 会删掉旧凭据;删除连同 secret |
|
|
| `POST /pi/upstreams/{id}/test {mode?: auth\|completion, model?}` | 缺省 `auth`:解析凭据(OAuth 必要时刷新)+ 校验模型在目录中,**不产生费用**;`completion` 再发一次最多 16 token 的补全 |
|
|
| `GET /pi/upstreams/{id}/models` | 静态目录(已配置凭据时按 provider 过滤,如 Copilot);Radius 先动态刷新,失败时回静态表并给 `refreshError` |
|
|
| `POST /pi/upstreams/{id}/oauth/start {mode?, enterpriseDomain?}` | 开始订阅登录,见下 |
|
|
| `POST /pi/upstreams/{id}/oauth/complete {sessionId, pastedUrlOrCode?, waitMs?}` | 完成 / 轮询登录 |
|
|
| `POST /pi/upstreams/{id}/oauth/logout` | 删除本地 token,状态回到 `auth_required`(不会撤销提供方侧授权) |
|
|
|
|
**上游状态**(服务端维护):`auth_required`(新建 oauth / 未填 key / 已登出)→ 登录或填 key → `ok`;测试失败 → `error`(凭据被拒 → `auth_required`);
|
|
OAuth 刷新失败 → `expired` + 待处理事项「模型上游 X 登录已失效,需要重新登录」(去重键 `pi_upstream_expired:<id>`),重新登录成功后事项自动关闭。
|
|
|
|
**订阅登录**(8 种:Anthropic Claude Pro/Max、OpenAI Codex、GitHub Copilot、xAI、Kimi Code、Meta Muse、OpenRouter、Radius;OpenCode Go / Zen 只有 API Key):
|
|
服务端调用 pi 的 `models.login`,注入挂起式交互 —— 选方式用 start 的 `mode` 作答,Copilot 的企业域名用 `enterpriseDomain` 作答。状态机:
|
|
|
|
1. `start` → `{sessionId, mode, state: pending, authorizeUrl | userCode + verificationUrl, expiresAt}`(会话 15 分钟)。
|
|
2. **browser**(Anthropic、OpenRouter、Codex / Radius 可选):用户在自己的浏览器打开 `authorizeUrl`,授权后会跳到 `localhost:<端口>`,页面打不开是预期的,
|
|
把**地址栏完整 URL** 粘回来 → `complete {sessionId, pastedUrlOrCode}`。state 不匹配、code 缺失返回 `failed` 与可读原因。
|
|
3. **device**(Copilot、xAI、Kimi、Meta,Codex / Radius 推荐):用户在 `verificationUrl` 输入 `userCode`;服务端轮询。前端反复调
|
|
`complete {sessionId}`,每次最多等 `waitMs`(缺省 25s),未完成返回 `pending`。Codex 设备码返回 404 表示账号未开启,改用 browser。
|
|
4. 终态:`succeeded`(token 包已加密写入,上游 `ok`)、`failed`、`expired`、`cancelled`(被同一上游的新登录、登出或删除取消)。
|
|
|
|
约束:同一 provider 的登录串行(另一个上游在登录中时 start 返回 409);pi 的浏览器流程会在服务端监听固定回调端口
|
|
(Anthropic 53692、Codex 1455、Radius 1456),端口被占用时返回「服务端回调端口 N 被占用」,容器里不要让别的进程占这些端口。
|
|
登录会话只在 api 进程内存里,多实例部署要把 `/pi/upstreams/*/oauth/*` 粘到同一实例。
|
|
**条款**:订阅登录借用的是官方 CLI / IDE 的客户端身份(Anthropic 还会伪装成 Claude Code),用于自动化后台服务可能违反提供方条款、有被限流或封号风险;
|
|
能力表每个订阅型 provider 都带 `termsNote`,设置页须展示。
|
|
|
|
**刷新**:定时任务 `pi.token_refresh`(维护组,默认每 10 分钟)入队 `refreshPiTokens`,对 30 分钟内过期的 OAuth 上游调 pi 的 `getAuth(minOAuthValidityMs)`。
|
|
pi 在我们的 `CredentialStore.modify` 里双重检查过期时间再刷新;`modify` 在事务里 `select … for update` 锁住上游行,
|
|
所以 api(测试端点)与 worker(Agent 运行时的按需刷新、定时任务)同时刷新也只刷新一次,轮换后的 refresh token 原子写回。
|
|
OpenRouter 换到的是永久 key,不刷新。
|
|
|
|
## 运行参数
|
|
|
|
```jsonc
|
|
{
|
|
"pi": {
|
|
"contextBudget": 128000, "compaction": { "enabled": true, "thresholdRatio": 0.8 },
|
|
"subagents": { "enabled": false, "maxConcurrent": 2, "maxDepth": 1 } // subagentsEnabled 是 enabled 的旧别名
|
|
},
|
|
"agent": {
|
|
"maxToolCalls": 40, "maxDurationSec": 900, // 单次运行预算
|
|
"maxTokensPerRun": null, "maxCostUsdPerRun": null,
|
|
"autoInvestigate": false, "investigateMaxToolCalls": 8,
|
|
"autoInvestigateMode": "investigate", // 或 "recover"(授权内恢复,见上一节)
|
|
"investigateMaxAttempts": 2, "recoverVerifySec": 600,
|
|
"goalPollSec": 60, "goalWaitHours": 72
|
|
},
|
|
"budget": { "agentMaxRunsPerDay": 50, "agentMaxCostUsdPerDay": 5 }
|
|
}
|
|
```
|
|
|
|
上下文预算裁剪模型的 contextWindow;思考档位取上游的 `thinkingLevel`,运行时夹到模型支持的档位。
|
|
|
|
- 会话目录由环境变量 `AGENT_SESSION_DIR` 指定,缺省为 `<cwd>/data/agent-sessions`。api 进程和 worker 进程要能读到同一个目录,这是崩溃恢复的前提。
|
|
- TMDB 密钥放在 `metadata.tmdbApiKey`(只写,存为 secret `metadata_api_key`),地址放在 `metadata.tmdbBaseUrl`(可选)。设置里没有时,回退到环境变量 `TMDB_API_KEY` / `TMDB_ACCESS_TOKEN` / `TMDB_BASE_URL`。
|
|
|
|
## 子代理
|
|
|
|
- 开关:`pi.subagents.enabled`(设置页「允许子代理」,旧字段 `subagentsEnabled` 同步),关闭时模型拿不到 `agent.spawn`,服务端也拒绝派生。
|
|
- 用法:会话调用 `agent.spawn {goal, capabilityAllowlist?, budget?, mode?, upstreamId?, await?}` 派生子会话(独立 agent_run、同一 runAgent 任务、独立会话文件),再用 `agent.await` 取各子会话的状态与汇报摘要(已脱敏)。
|
|
- 权限:子会话能力白名单必须是父会话的子集,只给方案的会话只能派生只给方案的子会话,子会话继承父会话发起人,授权校验再按全部祖先会话的白名单与类别取交集。
|
|
- 限制:同一父会话下同时运行的子会话不超过 `maxConcurrent`(多出的排队等名额,不占 worker 也不调用模型),派生层数不超过 `maxDepth`,一个会话最多派生 10 个子会话。
|
|
- 预算:子会话预算从父会话剩余预算划拨(缺省剩余的一半,请求超过剩余直接拒绝),子会话超预算只结束它自己;父会话的工具调用 / token / 费用预算把子孙会话的实际用量算在内。
|
|
- 取消:中止父会话会递归取消全部未结束的子孙会话(排队中的也一样)。
|
|
- 事件:子会话的开始、工具调用、结束按父会话 seq 镜像为 `subagent_spawned` / `subagent_event` / `subagent_completed`,SSE 上以 `agent.tool_call`(能力 `agent.spawn`)提示;会话详情返回 `parent`、`children` 与 `familyUsage`(会话树合计)。
|
|
- `agent.await` 在父会话的工具调用里轮询业务表等待,不调用模型,但等待期间父会话占着一个 Agent worker 槽位,超时(缺省 600 秒)返回 pending。
|
|
- 成本:每个子会话都是一次完整的模型会话(独立上下文,有自己的系统提示与工具定义),token 成本大致随子会话数线性增加,也计入每日 Agent 运行次数。
|
|
|
|
## fake 模式演示
|
|
|
|
在设置页新建一个 provider = fake、authKind = none 的上游并设为默认(没有任何上游时,旧的 `PATCH /settings {"pi":{"provider":"fake"}}` 仍然生效)。
|
|
fake 上游也支持 authKind = oauth,用来联调登录流程:browser 模式粘回任意带 `code` 与正确 `state` 的 URL,device 模式约 3 秒后自动批准。
|
|
|
|
```sh
|
|
curl -X POST localhost:4380/api/v1/agent/runs -H "Authorization: Bearer umt-dev-admin-key" \
|
|
-H 'content-type: application/json' -d '{"goal":"看看库里有什么","mode":"plan_only"}'
|
|
```
|
|
|
|
没有配置 `agent.fakeScript` 时,使用内置演示脚本:查一次库存,然后如实汇报,不做写操作。自定义脚本用 JSON 写,可以是单个对象或数组:
|
|
|
|
```json
|
|
{
|
|
"match": "收进来",
|
|
"steps": [
|
|
{ "text": "查库存", "toolCalls": [{ "capability": "inventory.query", "input": { "query": "沙丘2" } }] },
|
|
{ "toolCalls": [{ "capability": "download.plan",
|
|
"input": { "slotId": "{{inventory.query.items.0.slots.0.id}}", "sourceReleaseId": "<id>" } }] },
|
|
{ "toolCalls": [{ "capability": "download.submit", "input": { "planId": "{{download.plan.plan.id}}" } }] }
|
|
],
|
|
"finalText": "已提交"
|
|
}
|
|
```
|
|
|
|
`{{能力ID.路径}}` 取该能力最近一次成功调用结果里对应路径的值。测试直接给 `createAgentModule({ fakeScripts })` 注入 `FakeScript`,输入可以写成函数。
|
|
|
|
## 事件约定
|
|
|
|
没有单独的 `download.completed` / `organization.completed` 事件,用现有事件表达:
|
|
- 下载完成 = `download.status_changed` 且 `to=completed`(只发一次);
|
|
- 入库完成 = `organization.changed` 且 `status=succeeded`。
|
|
|
|
## e2e
|
|
|
|
`apps/server/test/e2e/`:每个文件使用独立的业务 schema 和引擎 schema,跑真实的四个模块、OpenWorkflow worker 和 outbox 中继,外部依赖用 testkit 的 mock(qB、TMDB、RSS),文件放在临时目录。
|
|
|
|
| 文件 | 内容 |
|
|
|---|---|
|
|
| `e1-rss-to-library` | RSS → 识别 → 策略 → 下载 → Copy 入库 → 整理记录增量 → Slot satisfied |
|
|
| `e2-agent-delegate` | 委托闭环:无授权时被拒,有授权时成功;汇报以整理记录为准 |
|
|
| `e3-background-continues` | 关闭聊天后台照样推进 |
|
|
| `e4-crash-recovery` | worker 子进程在 download.submit 时 kill -9,新 worker resume,qB 只收到一次 add |
|
|
| `e5-auto-investigate` | 识别歧义 → 有界调查,去重 |
|
|
| `e6-auto-recover` | qB missingFiles → download_failed → 授权内恢复(task.inspect → download.recheck → 系统核实关闭事项);无授权时降级只调查;换源被拒;去重;同根因尝试上限;预算超限保持待处理 |
|
|
| `e11-subagents` | 父会话派生两个只读子会话并等待后汇总;maxConcurrent=1 时串行;子会话超预算不影响父;合计用量;子会话事件进父事件流;越权 / 提权 / 深度 / 关闭被拒;父中止后子会话 cancelled |
|
|
|
|
## 已知限制
|
|
|
|
- 编译后的二进制(`deno compile`)只授予 `--allow-read=/app`,没有写权限。Agent 会话目录和入库 Copy/Move 在容器里都需要写权限,需要在 W4 统一补 `--allow-write=<数据目录,库根>`,在此之前生产容器里的 runAgent 会失败。
|
|
- 41 个 provider 中只有请求形状与凭据装配经过测试(fake、自定义兼容端点真实走 HTTP);各家真实上游与 8 种订阅登录没有联网实测,首次接入请用「测试」按钮的 completion 模式确认。
|
|
- 登录会话在 api 进程内存,进程重启后进行中的登录丢失,需要重新开始。
|
|
- 追加消息与正在收尾的 run 存在很小的竞态窗口。`finalize` 会检查一次未消费的消息并补唤醒一次 run。
|