- Capabilities: all 14 registered (AO: inventory.query, download.plan/submit, organize.plan/execute,
policy.preview/apply; agent: task.inspect/retry/cancel); startup asserts count == 14.
Registry injects server-side authorize (automation_grant by action/scope/policy version/budget,
one-shot approval bound to plan id + payload hash + revision + expiry; model claims ignored;
automation switch), audit_event, server idempotency keys and a capability_invocation op ledger
(migration 0021) used for crash reconciliation. download.plan creates draft plans only;
download.submit promotes draft -> reserved after authorization.
- Agent session API: POST/GET /agent/runs, GET /agent/runs/{id} (events by seq, messages, report),
POST .../messages (wake), POST .../abort; automation-grants, approvals, capabilities invoke, tasks.
- runAgent task: Pi or fake provider from settings (apiKey via secrets), events persisted to
agent_run_event then published, resume + ledger reconciliation when pendingOperations > 0,
waits for goal-linked downloads/imports and reports from business tables (submitted != imported).
- Background triggers: decision.created -> bounded plan_only investigation (dedupe per decision);
decision.updated(resolved) -> resolveDecision executes chosen option.
- W2 closure: all OpenAPI routes registered in contracts api.ts (+ subset test); TMDB key from
settings secret_ref with env fallback; PUT /sites/{id}/credentials documented as alias of
/site-accounts; completion events documented as existing status events.
- e2e (real PG + OpenWorkflow + testkit mocks): RSS->library, agent delegation with grant/deny,
background continuation, worker kill -9 recovery (single qB add), auto investigation dedupe.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
11 KiB
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。
能力
启动时装配 14 个能力(catalog.lookup、release.search、release.inspect、identity.resolve、download.plan、download.submit、organize.plan、organize.execute、task.inspect、task.retry、task.cancel、policy.preview、policy.apply、inventory.query)。注册数不等于 14 时进程启动失败。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} |
单次审批。批准时要回传 payloadHash,计划变化后旧审批失效 |
GET /capabilities、POST /capabilities/{id}/invoke |
能力目录与调用,HTTP scope 按能力描述符再校验一次 |
GET /tasks[/{id}]、POST /tasks/{id}/cancel、POST /tasks/{id}/retry |
业务任务 |
增量推送:事件先写 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 的完整事件。
runAgent 做什么
-
drive步骤:- 从设置读 Pi 参数(provider / baseUrl / model / 思考档位 / 上下文预算 / 压缩)和预算。
pi.apiKeySecretRef现场解密后传给 SDK,不落日志。 - 检查今日次数和费用预算(
budget.agentMaxRunsPerDay/agentMaxCostUsdPerDay)。 - 打开
AGENT_SESSION_DIR/<runId>.jsonl。pendingOperations>0说明上次崩溃:先按幂等键对账,再resume();否则就prompt(goal)。最后逐条消费追加的消息。
- 从设置读 Pi 参数(provider / baseUrl / model / 思考档位 / 上下文预算 / 压缩)和预算。
-
goal-settled步骤:只在 execute 模式且会话正常结束时执行。每隔agent.goalPollSec检查一次,直到本 run 触达的计划、下载和整理记录全部落定,最长等agent.goalWaitHours。等待期间 run 状态为waiting,汇报写的是"已提交≠已入库"。 -
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)。满足以下全部条件时,会发起一次有界调查 run:agent.autoInvestigate=true、kind 为identity_ambiguous/download_failed/download_stalled、事项仍为 open、事项不是由 Agent 自己引起的。这个 run 的参数:trigger=event、mode=plan_only,白名单为只读能力加identity.resolve,工具调用上限为agent.investigateMaxToolCalls,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 在处理端点里直接执行。 - 选项带
配置上游
在设置页或 PATCH /settings 里配置:
{
"pi": {
"provider": "anthropic", // 或 "openai-compatible"(baseUrl 要带 /v1);"fake" = 演示假模型
"baseUrl": null, // Anthropic 写根地址,不带 /v1
"model": "<模型 ID>",
"thinkingLevel": "medium",
"contextBudget": 128000,
"compaction": { "enabled": true, "thresholdRatio": 0.8 },
"apiKey": "<只写,存为 secret>"
},
"agent": {
"maxToolCalls": 40, "maxDurationSec": 900, // 单次运行预算
"maxTokensPerRun": null, "maxCostUsdPerRun": null,
"autoInvestigate": false, "investigateMaxToolCalls": 8,
"goalPollSec": 60, "goalWaitHours": 72
},
"budget": { "agentMaxRunsPerDay": 50, "agentMaxCostUsdPerDay": 5 }
}
- 会话目录由环境变量
AGENT_SESSION_DIR指定,缺省为<cwd>/data/agent-sessions。api 进程和 worker 进程要能读到同一个目录,这是崩溃恢复的前提。 - TMDB 密钥放在
metadata.tmdbApiKey(只写,存为 secretmetadata_api_key),地址放在metadata.tmdbBaseUrl(可选)。设置里没有时,回退到环境变量TMDB_API_KEY/TMDB_ACCESS_TOKEN/TMDB_BASE_URL。
fake 模式演示
curl -X PATCH localhost:4380/api/v1/settings -H "Authorization: Bearer umt-dev-admin-key" \
-H 'content-type: application/json' -d '{"pi":{"provider":"fake","model":"fake-model"}}'
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 写,可以是单个对象或数组:
{
"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 |
识别歧义 → 有界调查,去重 |
已知限制
- 编译后的二进制(
deno compile)只授予--allow-read=/app,没有写权限。Agent 会话目录和入库 Copy/Move 在容器里都需要写权限,需要在 W4 统一补--allow-write=<数据目录,库根>,在此之前生产容器里的 runAgent 会失败。 - 子代理(
subagentsEnabled)只做记录,没有实现。 - 追加消息与正在收尾的 run 存在很小的竞态窗口。
finalize会检查一次未消费的消息并补唤醒一次 run。