Skip to content

门面 Agent 与 AgentRuntime ​

门面 Agent 是平台托管的一类:没有外部端点、由 AgentRuntime 执行。 外部 Agent(供应商自有运行时)的接入见 外部 Agent 接入。

门面 Agent 是需求入口:它可以对话、可以登记需求、可以在本地完成交付,但不把对话原文写进状态层。


1. 定义与准入 ​

判定(TaskExecutionService.isPlatformHostedFacade / AgentDialogueService.resolveSessionRuntime):

  • AgentProfile.status = active;
  • 没有 a2aEndpoint;
  • 没有 gatewayUrl / dialogue.endpoint.url(没有可推送的 socket 客户端);
  • 通常带 stateContract。

delivery 决定它能不能承诺结果:

模式含义谁执行
local-skill有 skillIds,平台本地产出AgentRuntime
intake-only只登记需求,转人工/待招募无执行,返回登记结果
supplier-attached有 a2aEndpoint属于外部 Agent,不是门面

2. 三种执行入口 ​

入口调用方Runtime 方法输入输出
对话AgentDialogueServicerunStream() / run()截断后的历史对话回复;确认后 submit_request
状态唤醒AgentTriggerExecutor → Bull consumerrun()事件快照(契约 input schema)contract.output.path(envelope)
任务节点TaskExecutionService.executeLocalFacaderun()node.config.input节点结果(文本/对象)

三者共用同一个 AgentRuntime,但语义不同:

对话状态唤醒
MemorypersistMemory=false,原文不进状态层读/写 task memory
结构化自然语言 + submit_requestJSON envelope + Ajv 校验
重试用户重发默认不自动重试
生命周期多轮 session(轮次/TTL/token 预算)一次唤醒,跑完释放

3. AgentRuntime 能力矩阵 ​

能力行为
无会话一次 run() = 一次唤醒,无跨 run 隐式状态
预算maxSteps / maxTokens / deadlineMs,结构化停止
工具白名单 registry;extraTools 仅本次可见
Memorytask 级 working memory;对话关闭
并发per-agent(默认 10)+ per-tenant(默认 20),超限 429
取消AbortSignal 中断在途 LLM,半截输出不落库
流式runStream();端口无流时退回 complete()
结构化状态唤醒带 response_format: json_object + Ajv 校验
观测runs/latency/stopReasons/resources + Mongo 采样

4. 对话入口(HTTP + SSE) ​

bash
# 1) 创建/复用 session(runtime=local 由 profile 自动判定)
POST /api/gateway/dialogue/sessions
{ "agentId": "facade-hs-code", "taskId": "469...", "reuseActive": true }

# 2) 非流式一轮
POST /api/gateway/dialogue/sessions/{sessionId}/message
{ "text": "蓝牙耳机出口德国,HS Code 怎么归类?" }

# 3) 流式一轮(start → delta* → done → [result])
POST /api/gateway/dialogue/sessions/{sessionId}/message/stream
{ "text": "...", "waitForResult": true }

# 4) 结果回读(submit_request 之后)
GET /api/gateway/dialogue/sessions/{sessionId}/result

message 返回:

json
{
  "sessionId": "dialogue_...",
  "turnCount": 2,
  "response": "……",
  "status": "input_required",
  "done": false,
  "stopReason": "completed",
  "tokens": 1234,
  "submission": { "requestId": "...", "eventId": "...", "resultPath": "..." },
  "history": { "keptMessages": 4, "omittedMessages": 0, "estimatedTokens": 900 }
}

SSE 事件 ​

事件负载说明
start{ sessionId, at }一轮开始
delta{ delta }模型 content 流式片段
tool_call{ name, ok, durationMs }工具轮单独事件
done轮次结果 + submissionsubmission 是指针,不是结果
result状态层结果 + output本轮写了状态且下游已写出结果时推送
error{ message, status }业务错误;客户端断连不发
  • 断连即取消:连接 close 触发 AbortSignal,中断在途模型请求;这一轮不落库。
  • 心跳:AGENT_DIALOGUE_SSE_HEARTBEAT_MS(默认 15s)写 : ping。
  • 结果推送:done 后若存在 submission.resultPath,同一连接继续等待并推 result;GET .../result 是兜底。

确认后交接给状态层 ​

对话(多轮,原文不进状态层)
  → 用户确认
  → submit_request 按契约 input schema 校验
  → 写 /tasks/{taskId}/requests/{sessionId}(含 conversation_id + confirmed_by)
  → 订阅命中 → 下游写 resultPath
  → SSE result / GET .../result 回读

边界:没有 taskId 不暴露 submit_request;幂等是「读后写」检查,不是唯一索引。

5. 状态入口(契约 → 写入 → 订阅 → 回写) ​

ts
// 1) 定义契约(AgentProfile.stateContract)
{
  version: 2,
  input:  { path: '/tasks/{taskId}/requests/{requestId}', schema: { ... } },
  output: { path: '/tasks/{taskId}/agents/{agentId}/results/{eventId}', schema: envelope },
  trigger:{ pattern: '/tasks/*/requests/*' },
  delivery: { mode: 'local-skill' }
}

// 2) 发布订阅(编译契约快照)
await contractCompiler.apply(await contractCompiler.plan(agentId));

// 3) 写请求(批量/异步可选)
await state.write({ path: requestPath, value: { ... }, actor: { type: 'user', id } });
await state.writeMany(inputs, { dispatch: 'async' }); // 只写 outbox

执行链:

state_events / state_entries
  → StateTriggerDispatcher.matchEvent()
    → AgentTriggerExecutor.execute()
      → Bull agent-execution(默认)
        → AgentExecutionConsumer
          → AgentRuntime.run({ responseFormat: 'json_object' })
          → parse + Ajv validate
          → write contract.output.path(envelope)
      → 失败写 .../errors/{eventId},job failed
  • dispatch: 'sync'(默认)提交后立即匹配订阅并入队;async 只写 state_outbox,由 worker 按 namespace/seq 顺序派发。
  • triggerDepth 每次 +1;达到 maxTriggerDepth 只记事件、不再触发(防环)。
  • 没有已发布订阅的门面不会被状态唤醒;迁移/改契约后需要重新 apply。

6. 结构化交付(envelope + JSON mode) ​

门面的状态交付统一为:

json
{
  "status": "ok | needs_input | intake | not_applicable",
  "summary": "一句话结论",
  "data": { "...业务字段..." },
  "follow_up": ["..."],
  "confidence": 0.72,
  "text": "长报告原文(可选)"
}
  • data 来自该门面第一个 skill 的 output_schema;intake-only 门面允许任意对象。
  • 状态唤醒 prompt 强制“只返回一个符合 schema 的 JSON 对象”;请求带 response_format: { type: 'json_object' }。
  • 供应商边界(实测):DeepSeek 支持 json_object,json_schema strict 返回 400;字段级约束由平台 Ajv 兜底。
  • 解析支持纯 JSON、```json 围栏、前后夹说明文字;失败写 errors/{eventId}。
  • 对话模式不启用 JSON mode(需要自然语言与澄清)。

7. 资源边界与错误语义 ​

场景表现
per-agent / per-tenant 并发超限HTTP 429,code=agent_runtime_busy
单租户活跃对话 session 超限HTTP 429,code=dialogue_session_quota_exceeded
空闲超过 TTLHTTP 410 Session expired
达到 maxTurns / maxTokensPerSessionHTTP 400
客户端断连中断在途 LLM,不发 SSE error,这一轮不落库
预算耗尽stopReason = max_steps / max_tokens / deadline
输出不符合契约errors/{eventId} + 抛出(Bull failed)

对话预算(新建 session 时冻结):

AGENT_DIALOGUE_MAX_TURNS=12
AGENT_DIALOGUE_MAX_HISTORY_TOKENS=8000
AGENT_DIALOGUE_IDLE_TTL_MS=1800000
AGENT_DIALOGUE_MAX_TOKENS_PER_SESSION=50000
AGENT_DIALOGUE_MAX_SESSIONS_PER_TENANT=100
AGENT_DIALOGUE_SSE_HEARTBEAT_MS=15000
AGENT_DIALOGUE_RESULT_PUSH_TIMEOUT_MS=30000

单轮运行时预算:maxSteps=3、maxTokens=6000、墙钟 60s。

8. 性能(2026-10-23 实测,非 mock) ​

场景结果
AgentRuntime.run() 单次(简单 prompt)最快 1.08s;同一 prompt 也出现过 9.7s(模型侧波动)
对话一轮(本地门面)2.7–3.4s
状态唤醒 write() sync写入 32ms,端到端最快 1.53s
状态唤醒 writeMany async ×5写入 65ms,端到端 5.0–7.1s
任务节点(本地门面)5.5–7.3s(含任务旁路)

表中不含外部 A2A 供应商的延迟:那由供应商自己的运行时决定(进程冷启动、session、模型排队),属于供应商运维范围,不是 AgentRuntime 的指标。

运行时基准(mock LLM,不烧 token):

bash
npm run bench:agent-runtime
BENCH_RUNS=5000 BENCH_CONCURRENCY=200 npm run bench:agent-runtime

9. 监控与配置 ​

方法路径说明
GET/api/agent-runtime/metrics实时快照(进程内)
GET/api/agent-runtime/history?minutes=60Mongo 历史采样
POST/api/agent-runtime/benchmark内置基准(互斥)
POST/api/agent-runtime/metrics/reset清空内存计数
页面/dashboard/agent-runtimeRuntime metrics

指标:runs/success/failure/inFlight、p50/p95/p99、stopReasons、throughput、heap/RSS、event loop lag。只记录元数据,不落 prompt/工具结果/业务内容。

10. 已知边界 ​

  • 配额是进程内计数。PM2 当前单实例 fork,数字准确;多实例严格全局上限需要 Redis 或集中计数。
  • outbox worker 只在实例 0 运行;扩多实例前需要 namespace lease 或独立 worker。
  • DeepSeek 只支持 json_object,不支持 json_schema strict;模型可能返回 needs_input 而不是 ok。
  • 状态唤醒不自动重试:重复执行会重复烧 token。
  • 对话原文不进状态层:只写确认后的结构化 request/result。

下一步 ​

Released under the MIT License.