Skip to content

fix(agent): persist agent token usage#38270

Merged
zyssyz123 merged 2 commits into
feat/agent-v2from
fix/agent-token-usage-feat
Jul 1, 2026
Merged

fix(agent): persist agent token usage#38270
zyssyz123 merged 2 commits into
feat/agent-v2from
fix/agent-token-usage-feat

Conversation

@zyssyz123

Copy link
Copy Markdown
Contributor

Summary

  • Add token usage to dify-agent run_succeeded terminal events.
  • Map agent backend usage through the API event adapter and persist it in Agent App message usage.
  • Preserve terminal usage in Workflow Agent Node metadata for downstream output adaptation.
  • Make Agent Roster monitoring source=all include debugger/build-preview messages, so the monitoring page reflects Build/Preview usage.

Root Cause

The issue was backend-side, not frontend-side:

  1. dify-agent had usage available from agent.run(...), but did not expose it in the public terminal success event.
  2. API Agent App runner always ended messages with LLMUsage.empty_usage(), so messages.message_tokens and messages.answer_tokens stayed at 0.
  3. Monitoring summary aggregates token fields from messages; after token persistence was fixed, source=all still excluded debugger messages, so Build/Preview usage remained hidden unless source=debugger was 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 -q
  • cd 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.py
  • cd 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.py
  • git diff --check origin/feat/agent-v2...HEAD

API pytest is currently failing locally with exit 139 during pytest startup in this environment, before test assertions run.

Remote dev validation on agent.dify.dev after deploying deploy/agent:

  • API and agent backend containers updated to ce01bb7ad447ba28f61cd2e23d3721648b9a5176.
  • Real Agent chat returned message_end.metadata.usage with non-zero tokens.
  • Latest DB message persisted message_tokens=3471, answer_tokens=18.
  • Monitoring GET /console/api/agent/{agent_id}/statistics/summary?source=all returned total_tokens=6943 and non-zero tokens_per_second.

@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Diff

base → 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]

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Pyrefly Type Coverage

Metric Base PR Delta
Type coverage 51.86% 51.86% +0.00%
Strict coverage 51.38% 51.38% +0.00%
Typed symbols 31,518 31,522 +4
Untyped symbols 29,533 29,533 0
Modules 2952 2952 0

@dosubot

dosubot Bot commented Jul 1, 2026

Copy link
Copy Markdown

📄 Knowledge review

✏️ Suggested updates

1 page suggestion needs review.

Page Library Status
Dify Agent Server 模块分析 (Commit 55f95dbc) dify 🟡 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 配置、执行上下文和退出策略
 

Accept · Edit · Decline


Leave Feedback Ask Dosu about dify

@zyssyz123 zyssyz123 merged commit c63037a into feat/agent-v2 Jul 1, 2026
4 checks passed
@zyssyz123 zyssyz123 deleted the fix/agent-token-usage-feat branch July 1, 2026 10:29
@lyzno1 lyzno1 mentioned this pull request Jul 2, 2026
5 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant