fix(agent): persist agent token usage#38270
Merged
Merged
Conversation
Contributor
Pyrefly Diffbase → PR--- /tmp/pyrefly_base.txt 2026-07-01 10:10:26.474279902 +0000
+++ /tmp/pyrefly_pr.txt 2026-07-01 10:10:17.794190543 +0000
@@ -1913,17 +1913,17 @@
ERROR Argument `dict[str, object]` is not assignable to parameter `configs` with type `Mapping[str, LayerConfigInput] | None` in function `agenton.compositor.core.Compositor._create_run` [bad-argument-type]
--> tests/unit_tests/clients/agent_backend/test_cleanup_composition_compositor_integration.py:97:17
ERROR Cannot index into `bool` [bad-index]
- --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:53:12
+ --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:54:12
ERROR Cannot index into `float` [bad-index]
- --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:53:12
+ --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:54:12
ERROR Cannot index into `int` [bad-index]
- --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:53:12
+ --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:54:12
ERROR Cannot index into `list[JsonValue]` [bad-index]
- --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:53:12
+ --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:54:12
ERROR Cannot index into `str` [bad-index]
- --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:53:12
+ --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:54:12
ERROR `None` is not subscriptable [unsupported-operation]
- --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:53:12
+ --> tests/unit_tests/clients/agent_backend/test_event_adapter.py:54:12
ERROR Expected a callable, got `None` [not-callable]
--> tests/unit_tests/commands/test_clean_expired_messages.py:33:9
ERROR Expected a callable, got `None` [not-callable]
@@ -3230,13 +3230,19 @@
ERROR Argument `DummyAccount` is not assignable to parameter `user` with type `Account | EndUser` in function `core.app.apps.agent_app.app_generator.AgentAppGenerator._run_stateless` [bad-argument-type]
--> tests/unit_tests/core/app/apps/agent_app/test_app_generator.py:258:18
ERROR Object of class `NoneType` has no attribute `message` [missing-attribute]
- --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:360:12
+ --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:362:12
ERROR Object of class `NoneType` has no attribute `model` [missing-attribute]
- --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:361:12
+ --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:363:12
ERROR Object of class `NoneType` has no attribute `message` [missing-attribute]
- --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:394:12
+ --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:396:12
+ERROR Object of class `NoneType` has no attribute `usage` [missing-attribute]
+ --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:397:12
+ERROR Object of class `NoneType` has no attribute `usage` [missing-attribute]
+ --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:398:12
+ERROR Object of class `NoneType` has no attribute `usage` [missing-attribute]
+ --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:399:12
ERROR Object of class `NoneType` has no attribute `message` [missing-attribute]
- --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:409:12
+ --> tests/unit_tests/core/app/apps/agent_app/test_app_runner.py:414:12
ERROR Object of class `NoneType` has no attribute `message` [missing-attribute]
--> tests/unit_tests/core/app/apps/agent_app/test_input_guards.py:67:12
ERROR Object of class `NoneType` has no attribute `prompt_messages` [missing-attribute]
|
Contributor
Pyrefly Type Coverage
|
📄 Knowledge review✏️ Suggested updates1 page suggestion needs review.
📝 Dify Agent Server 模块分析 (Commit 55f95db)@@ -694,6 +694,18 @@
事件类型使用 Pydantic 的 `discriminator="type"` 机制,客户端可通过 `RUN_EVENT_ADAPTER.validate_json()` 反序列化为正确的具体类型 [[44]](https://github.com/langgenius/dify/blob/7210f856c9c07ae03d7c2e5def96c949efee6397/dify-agent/src/dify_agent/protocol/schemas.py#L236-L240)。
+### Token 使用统计
+
+**`AgentRunUsage`**:记录单次 Agent 运行的 token 消耗统计。
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| `prompt_tokens` | `int` | 输入 token 数(默认 0)|
+| `completion_tokens` | `int` | 输出 token 数(默认 0)|
+| `total_tokens` | `int` | 总 token 数,若为 0 且前两者非零则自动计算 |
+
+该模型使用 `@model_validator(mode="after")` 自动派生 `total_tokens`,确保 `prompt_tokens + completion_tokens = total_tokens` 的一致性。
+
### 四种事件类型
#### 1. `run_started` — 运行开始
@@ -736,6 +748,11 @@
"session_snapshot": {
"schema_version": 1,
"layers": [ { "name": "prompt", "lifecycle_state": "suspended", "runtime_state": {} }, ... ]
+ },
+ "usage": {
+ "prompt_tokens": 3471,
+ "completion_tokens": 18,
+ "total_tokens": 3489
}
},
"created_at": "2026-05-14T06:20:05Z"
@@ -744,12 +761,13 @@
**终端成功事件的设计要点**([[47]](https://github.com/langgenius/dify/blob/7210f856c9c07ae03d7c2e5def96c949efee6397/dify-agent/src/dify_agent/protocol/schemas.py#L22-L31)):最终输出和可恢复的 Agenton 会话快照**一同包含在同一事件中**。消费者可将终端事件视为运行的完整摘要,无需关联多个不同的载荷事件。
-`RunSucceededEventData` 现在具有两个互斥的结果字段:
+`RunSucceededEventData` 现在具有三个可选结果字段:
- **`output: JsonValue | None`**:最终 JSON-safe 输出(对于非结构化文本为字符串,对于结构化输出为 JSON 对象)
- **`deferred_tool_call: DeferredToolCallPayload | None`**:外部工具请求载荷(用于 ask-human 流程)
+- **`usage: AgentRunUsage | None`**:token 使用统计(由底层模型推理产生)
- **`session_snapshot: CompositorSessionSnapshot`**:总是存在
-**有且仅有** `output` 或 `deferred_tool_call` 之一会被设置。当 ask-human 层激活且模型请求人工输入时,事件将包含 `deferred_tool_call` 而非 `output`;正常完成时包含 `output`。
+**有且仅有** `output` 或 `deferred_tool_call` 之一会被设置。当 ask-human 层激活且模型请求人工输入时,事件将包含 `deferred_tool_call` 而非 `output`;正常完成时包含 `output`。`usage` 字段在所有成功完成路径中可选出现(取决于模型是否返回使用统计)。
#### 4. `run_failed` — 运行失败(终端事件)
@@ -1635,7 +1653,7 @@
- `DifyAgentTimeoutError` → `AgentBackendTransportError`
- `DifyAgentStreamError` → `AgentBackendStreamError`
-**事件适配器**(`event_adapter.py`):将 `dify-agent` 的事件流转换为工作流节点可消费的格式
+**事件适配器**(`event_adapter.py`):将 `dify-agent` 的事件流转换为工作流节点可消费的格式。适配器在终端成功事件(`AgentBackendRunSucceededInternalEvent`)和延迟工具调用事件(`AgentBackendDeferredToolCallInternalEvent`)中包含可选的 `usage` 字段,通过 `_agent_run_usage()` 辅助函数将 Agent 后端的 usage 元数据转换为 JSON 安全的字典格式,供下游消费者使用
**请求构建器**(`request_builder.py`):封装 `CreateRunRequest` 构造逻辑,处理 Layer 配置、执行上下文和退出策略
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
run_succeededterminal events.source=allinclude debugger/build-preview messages, so the monitoring page reflects Build/Preview usage.Root Cause
The issue was backend-side, not frontend-side:
agent.run(...), but did not expose it in the public terminal success event.LLMUsage.empty_usage(), somessages.message_tokensandmessages.answer_tokensstayed at 0.messages; after token persistence was fixed,source=allstill excluded debugger messages, so Build/Preview usage remained hidden unlesssource=debuggerwas selected.Verification
cd dify-agent && uv run pytest tests/local/dify_agent/protocol/test_protocol_schemas.py::test_run_succeeded_event_round_trips_usage tests/local/dify_agent/runtime/test_runner.py::test_runner_emits_terminal_success_and_snapshot tests/local/dify_agent/runtime/test_runner.py::test_runner_preserves_explicit_json_null_output tests/local/dify_agent/runtime/test_runner.py::test_runner_emits_deferred_tool_call_and_persists_pending_history tests/local/dify_agent/runtime/test_runner.py::test_runner_passes_dynamic_dify_knowledge_tools_to_agent -qcd api && uv run ruff check clients/agent_backend/event_adapter.py core/app/apps/agent_app/app_runner.py core/workflow/nodes/agent_v2/output_adapter.py services/agent/observability_service.py tests/unit_tests/clients/agent_backend/test_event_adapter.py tests/unit_tests/core/app/apps/agent_app/test_app_runner.py tests/unit_tests/services/agent/test_agent_observability_service.pycd api && uv run pyrefly check clients/agent_backend/event_adapter.py core/app/apps/agent_app/app_runner.py core/workflow/nodes/agent_v2/output_adapter.py services/agent/observability_service.pygit diff --check origin/feat/agent-v2...HEADAPI pytest is currently failing locally with exit 139 during pytest startup in this environment, before test assertions run.
Remote dev validation on
agent.dify.devafter deployingdeploy/agent:ce01bb7ad447ba28f61cd2e23d3721648b9a5176.message_end.metadata.usagewith non-zero tokens.message_tokens=3471,answer_tokens=18.GET /console/api/agent/{agent_id}/statistics/summary?source=allreturnedtotal_tokens=6943and non-zerotokens_per_second.