From 52c134fdb8ac1b9299334b89f964cfdb5ab5feb8 Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Tue, 24 Feb 2026 15:20:22 +0800 Subject: [PATCH 1/4] update --- .github/PULL_REQUEST_TEMPLATE.md | 2 + backend/app/agent/listen_chat_agent.py | 51 ++++++++++++++++++++++++- backend/app/service/chat_service.py | 2 + backend/app/service/task.py | 17 +++++++++ server/tests/test_auth.py | 8 ++-- server/tests/test_chat_share.py | 2 - server/tests/test_proxy_controller.py | 4 +- src/store/chatStore.ts | 8 ++++ src/types/chatbox.d.ts | 3 ++ src/types/constants.ts | 1 + test/unit/store/chatStore.test.ts | 53 ++++++++++++++++++++++++++ 11 files changed, 139 insertions(+), 12 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 762f852b3..f711a60c9 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -15,7 +15,9 @@ Closes # ### Testing Evidence (REQUIRED) + + - [ ] I have included human-verified testing evidence in this PR. diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index 6bdb61ca5..fc8ae53ec 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -13,6 +13,7 @@ # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import asyncio +import inspect import json import logging from collections.abc import Callable @@ -42,6 +43,7 @@ ActionBudgetNotEnough, ActionDeactivateAgentData, ActionDeactivateToolkitData, + ActionRequestUsageData, get_task_lock, set_process_task, ) @@ -93,6 +95,17 @@ def __init__( step_timeout: float | None = 1800, # 30 minutes **kwargs: Any, ) -> None: + self.api_task_id = api_task_id + self.agent_name = agent_name + self._request_usage_events_enabled = False + self._user_on_request_usage = kwargs.pop("on_request_usage", None) + if ( + "on_request_usage" + in inspect.signature(ChatAgent.__init__).parameters + ): + kwargs["on_request_usage"] = self._on_request_usage + self._request_usage_events_enabled = True + super().__init__( system_message=system_message, model=model, @@ -116,11 +129,41 @@ def __init__( step_timeout=step_timeout, **kwargs, ) - self.api_task_id = api_task_id - self.agent_name = agent_name process_task_id: str = "" + def _on_request_usage(self, payload: dict[str, Any]) -> Any: + request_usage = payload.get("request_usage") or {} + step_usage = payload.get("step_usage") or {} + request_tokens = int(request_usage.get("total_tokens") or 0) + if request_tokens > 0: + task_lock = get_task_lock(self.api_task_id) + _schedule_async_task( + task_lock.put_queue( + ActionRequestUsageData( + data={ + "agent_name": self.agent_name, + "process_task_id": self.process_task_id, + "agent_id": self.agent_id, + "tokens": request_tokens, + "request_index": int( + payload.get("request_index") or 0 + ), + "response_id": str( + payload.get("response_id") or "" + ), + "step_total_tokens": int( + step_usage.get("total_tokens") or 0 + ), + } + ) + ) + ) + + if self._user_on_request_usage is not None: + return self._user_on_request_usage(payload) + return None + def _send_agent_deactivate(self, message: str, tokens: int) -> None: """Send agent deactivation event to the frontend. @@ -291,6 +334,8 @@ def step( f"Agent {self.agent_name} completed step, " f"tokens used: {total_tokens}" ) + if self._request_usage_events_enabled: + total_tokens = 0 assert message is not None @@ -394,6 +439,8 @@ async def astep( f"Agent {self.agent_name} completed step, " f"tokens used: {total_tokens}" ) + if self._request_usage_events_enabled: + total_tokens = 0 # Send deactivation for all non-streaming cases (success or error) # Streaming responses handle deactivation in _astream_chunks diff --git a/backend/app/service/chat_service.py b/backend/app/service/chat_service.py index b3a24d1f4..1aaf25514 100644 --- a/backend/app/service/chat_service.py +++ b/backend/app/service/chat_service.py @@ -1475,6 +1475,8 @@ def on_stream_text(chunk): yield sse_json("activate_agent", item.data) elif item.action == Action.deactivate_agent: yield sse_json("deactivate_agent", dict(item.data)) + elif item.action == Action.request_usage: + yield sse_json("request_usage", dict(item.data)) elif item.action == Action.assign_task: yield sse_json("assign_task", item.data) elif item.action == Action.activate_toolkit: diff --git a/backend/app/service/task.py b/backend/app/service/task.py index 604fbc717..eee60bfb4 100644 --- a/backend/app/service/task.py +++ b/backend/app/service/task.py @@ -49,6 +49,7 @@ class Action(str, Enum): create_agent = "create_agent" # backend -> user activate_agent = "activate_agent" # backend -> user deactivate_agent = "deactivate_agent" # backend -> user + request_usage = "request_usage" # backend -> user assign_task = "assign_task" # backend -> user activate_toolkit = "activate_toolkit" # backend -> user deactivate_toolkit = "deactivate_toolkit" # backend -> user @@ -155,6 +156,21 @@ class ActionDeactivateAgentData(BaseModel): data: DataDict +class RequestUsageDataDict(TypedDict): + agent_name: str + agent_id: str + process_task_id: str + tokens: int + request_index: int + response_id: str + step_total_tokens: int + + +class ActionRequestUsageData(BaseModel): + action: Literal[Action.request_usage] = Action.request_usage + data: RequestUsageDataDict + + class ActionAssignTaskData(BaseModel): action: Literal[Action.assign_task] = Action.assign_task data: dict[ @@ -288,6 +304,7 @@ class ActionSkipTaskData(BaseModel): | ActionCreateAgentData | ActionActivateAgentData | ActionDeactivateAgentData + | ActionRequestUsageData | ActionAssignTaskData | ActionActivateToolkitData | ActionDeactivateToolkitData diff --git a/server/tests/test_auth.py b/server/tests/test_auth.py index ffcb27946..9ffb1aecc 100644 --- a/server/tests/test_auth.py +++ b/server/tests/test_auth.py @@ -47,7 +47,7 @@ def test_auth_must_raises_on_none_token(self): """auth_must should raise TokenException immediately when token is None, not pass it to jwt.decode().""" import asyncio - from unittest.mock import MagicMock, patch + from unittest.mock import MagicMock from app.component.auth import auth_must from app.exception.exception import TokenException @@ -88,8 +88,7 @@ def test_list_snapshots_requires_auth_dependency(self): sig = inspect.signature(list_chat_snapshots) param_names = list(sig.parameters.keys()) assert "auth" in param_names, ( - "list_chat_snapshots is missing the 'auth' parameter — " - "unauthenticated users can list all snapshots" + "list_chat_snapshots is missing the 'auth' parameter — unauthenticated users can list all snapshots" ) def test_get_snapshot_requires_auth_dependency(self): @@ -130,8 +129,7 @@ def test_create_share_link_requires_auth_dependency(): sig = inspect.signature(create_share_link) param_names = list(sig.parameters.keys()) assert "auth" in param_names, ( - "create_share_link is missing the 'auth' parameter — " - "unauthenticated users can generate share tokens" + "create_share_link is missing the 'auth' parameter — unauthenticated users can generate share tokens" ) diff --git a/server/tests/test_chat_share.py b/server/tests/test_chat_share.py index 5af72b43e..6657f3451 100644 --- a/server/tests/test_chat_share.py +++ b/server/tests/test_chat_share.py @@ -15,8 +15,6 @@ import os from unittest.mock import patch -import pytest - class TestChatShareSecretKey: """Tests for ChatShare secret key generation. diff --git a/server/tests/test_proxy_controller.py b/server/tests/test_proxy_controller.py index c2ca19df2..1f2fa4242 100644 --- a/server/tests/test_proxy_controller.py +++ b/server/tests/test_proxy_controller.py @@ -12,9 +12,7 @@ # limitations under the License. # ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= -from urllib.parse import quote_plus, urlparse, parse_qs - -import pytest +from urllib.parse import parse_qs, quote_plus, urlparse class TestGoogleSearchUrlEncoding: diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 547f9af4e..dcb1ffb75 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -1345,6 +1345,14 @@ const chatStore = (initial?: Partial) => return; } + // Request-level token usage updates (non-stream mode) + if (agentMessages.step === AgentStep.REQUEST_USAGE) { + if (agentMessages.data.tokens) { + addTokens(currentTaskId, agentMessages.data.tokens); + } + return; + } + // Activate agent if ( agentMessages.step === AgentStep.ACTIVATE_AGENT || diff --git a/src/types/chatbox.d.ts b/src/types/chatbox.d.ts index 40c7d1a72..238258aac 100644 --- a/src/types/chatbox.d.ts +++ b/src/types/chatbox.d.ts @@ -139,6 +139,9 @@ declare global { agent?: string; file_path?: string; process_task_id?: string; + request_index?: number; + response_id?: string; + step_total_tokens?: number; output?: string; result?: string; tools?: string[]; diff --git a/src/types/constants.ts b/src/types/constants.ts index eba956f65..dd6aef033 100644 --- a/src/types/constants.ts +++ b/src/types/constants.ts @@ -26,6 +26,7 @@ export const AgentStep = { TASK_STATE: 'task_state', ACTIVATE_AGENT: 'activate_agent', DEACTIVATE_AGENT: 'deactivate_agent', + REQUEST_USAGE: 'request_usage', ASSIGN_TASK: 'assign_task', ACTIVATE_TOOLKIT: 'activate_toolkit', DEACTIVATE_TOOLKIT: 'deactivate_toolkit', diff --git a/test/unit/store/chatStore.test.ts b/test/unit/store/chatStore.test.ts index 1634667bb..793167946 100644 --- a/test/unit/store/chatStore.test.ts +++ b/test/unit/store/chatStore.test.ts @@ -579,6 +579,59 @@ describe('ChatStore - Core Functionality', () => { }); }); + describe('SSE request usage events', () => { + it('should accumulate tokens from request_usage event in non-stream mode', async () => { + vi.mocked(proxyFetchGet).mockImplementation((url: string) => + url?.includes?.('snapshots') + ? Promise.resolve([]) + : Promise.resolve({ + value: '', + api_url: '', + items: [], + warning_code: null, + }) + ); + + const mockFetchEventSource = vi.mocked(fetchEventSource); + mockFetchEventSource.mockImplementation(async (_url, opts) => { + opts.onmessage?.({ + data: JSON.stringify({ + step: 'request_usage', + data: { tokens: 11 }, + }), + } as any); + opts.onmessage?.({ + data: JSON.stringify({ + step: 'deactivate_agent', + data: { tokens: 0 }, + }), + } as any); + return Promise.resolve(); + }); + + const { result } = renderHook(() => useChatStore()); + let taskId!: string; + await act(async () => { + taskId = result.current.getState().create(); + result.current.getState().setActiveTaskId(taskId); + result.current.getState().setHasMessages(taskId, true); + result.current.getState().addMessages(taskId, { + id: generateUniqueId(), + role: 'user', + content: 'Test message', + }); + }); + + await act(async () => { + await result.current + .getState() + .startTask(taskId, 'replay', undefined, 0.2); + }); + + expect(result.current.getState().tasks[taskId].tokens).toBe(11); + }); + }); + describe('Replay', () => { const replayProjectState = () => ({ activeProjectId: 'proj-replay', From 6268ac0140c5ebbdf1e254825e73fa5d622e9d3a Mon Sep 17 00:00:00 2001 From: Sun Tao <2605127667@qq.com> Date: Tue, 24 Feb 2026 17:08:23 +0800 Subject: [PATCH 2/4] Update PULL_REQUEST_TEMPLATE.md --- .github/PULL_REQUEST_TEMPLATE.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f711a60c9..e7456f18f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,8 @@ -### Related Issue +# Pull Request + +## Related Issue @@ -8,11 +10,11 @@ Closes # -### Description +## Description -### Testing Evidence (REQUIRED) +## Testing Evidence (REQUIRED) @@ -24,13 +26,13 @@ Closes # - [ ] This PR includes frontend/UI changes, and I attached screenshot(s) or screen recording(s). - [ ] No frontend/UI changes in this PR. -### What is the purpose of this pull request? +## What is the purpose of this pull request? - [ ] Bug fix - [ ] New Feature - [ ] Documentation update - [ ] Other -### Contribution Guidelines Acknowledgement +## Contribution Guidelines Acknowledgement - [ ] I have read and agree to the [Eigent Contribution Guideline](https://github.com/eigent-ai/eigent/blob/main/CONTRIBUTING.md#eigent-contribution-guideline) From d242c5b04902e0dda8b2d44e939aaddbb0d907d9 Mon Sep 17 00:00:00 2001 From: Wendong-Fan Date: Thu, 5 Mar 2026 00:55:28 +0800 Subject: [PATCH 3/4] fix and enhance --- backend/app/agent/listen_chat_agent.py | 32 ++++++++++++-------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index 524199dbb..1cc79c17e 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -59,6 +59,10 @@ class ListenChatAgent(ChatAgent): threading.Lock() ) # Protects CDP URL mutation during clone + _camel_has_request_usage: bool = ( + "on_request_usage" in inspect.signature(ChatAgent.__init__).parameters + ) + def __init__( self, api_task_id: str, @@ -102,14 +106,9 @@ def __init__( ) -> None: self.api_task_id = api_task_id self.agent_name = agent_name - self._request_usage_events_enabled = False self._user_on_request_usage = kwargs.pop("on_request_usage", None) - if ( - "on_request_usage" - in inspect.signature(ChatAgent.__init__).parameters - ): + if self._camel_has_request_usage: kwargs["on_request_usage"] = self._on_request_usage - self._request_usage_events_enabled = True super().__init__( system_message=system_message, @@ -142,21 +141,16 @@ def _on_request_usage(self, payload: dict[str, Any]) -> Any: step_usage = payload.get("step_usage") or {} request_tokens = int(request_usage.get("total_tokens") or 0) if request_tokens > 0: - task_lock = get_task_lock(self.api_task_id) _schedule_async_task( - task_lock.put_queue( + get_task_lock(self.api_task_id).put_queue( ActionRequestUsageData( data={ "agent_name": self.agent_name, "process_task_id": self.process_task_id, "agent_id": self.agent_id, "tokens": request_tokens, - "request_index": int( - payload.get("request_index") or 0 - ), - "response_id": str( - payload.get("response_id") or "" - ), + "request_index": payload.get("request_index", 0), + "response_id": payload.get("response_id", ""), "step_total_tokens": int( step_usage.get("total_tokens") or 0 ), @@ -164,7 +158,6 @@ def _on_request_usage(self, payload: dict[str, Any]) -> Any: ) ) ) - if self._user_on_request_usage is not None: return self._user_on_request_usage(payload) return None @@ -339,7 +332,7 @@ def step( f"Agent {self.agent_name} completed step, " f"tokens used: {total_tokens}" ) - if self._request_usage_events_enabled: + if self._camel_has_request_usage: total_tokens = 0 assert message is not None @@ -444,7 +437,7 @@ async def astep( f"Agent {self.agent_name} completed step, " f"tokens used: {total_tokens}" ) - if self._request_usage_events_enabled: + if self._camel_has_request_usage: total_tokens = 0 # Send deactivation for all non-streaming cases (success or error) @@ -801,6 +794,10 @@ def clone(self, with_memory: bool = False) -> ChatAgent: else: cloned_tools, toolkits_to_register = self._clone_tools() + clone_kwargs: dict[str, Any] = {} + if self._user_on_request_usage is not None: + clone_kwargs["on_request_usage"] = self._user_on_request_usage + new_agent = ListenChatAgent( api_task_id=self.api_task_id, agent_name=self.agent_name, @@ -828,6 +825,7 @@ def clone(self, with_memory: bool = False) -> ChatAgent: enable_snapshot_clean=self._enable_snapshot_clean, step_timeout=self.step_timeout, stream_accumulate=self.stream_accumulate, + **clone_kwargs, ) new_agent.process_task_id = self.process_task_id From 7d80b1e74b3c0b5448ca592d0a80ddee3010975d Mon Sep 17 00:00:00 2001 From: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Date: Thu, 5 Mar 2026 09:09:40 +0000 Subject: [PATCH 4/4] enhance: add_request_level_token_callback PR1362 (#1439) --- backend/app/agent/listen_chat_agent.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/backend/app/agent/listen_chat_agent.py b/backend/app/agent/listen_chat_agent.py index 1cc79c17e..5d301d829 100644 --- a/backend/app/agent/listen_chat_agent.py +++ b/backend/app/agent/listen_chat_agent.py @@ -169,6 +169,8 @@ def _send_agent_deactivate(self, message: str, tokens: int) -> None: message: The accumulated message content tokens: The total token count used """ + if self._camel_has_request_usage: + tokens = 0 task_lock = get_task_lock(self.api_task_id) _schedule_async_task( task_lock.put_queue( @@ -332,24 +334,10 @@ def step( f"Agent {self.agent_name} completed step, " f"tokens used: {total_tokens}" ) - if self._camel_has_request_usage: - total_tokens = 0 assert message is not None - _schedule_async_task( - task_lock.put_queue( - ActionDeactivateAgentData( - data={ - "agent_name": self.agent_name, - "process_task_id": self.process_task_id, - "agent_id": self.agent_id, - "message": message, - "tokens": total_tokens, - }, - ) - ) - ) + self._send_agent_deactivate(message, total_tokens) if error_info is not None: raise error_info @@ -437,8 +425,6 @@ async def astep( f"Agent {self.agent_name} completed step, " f"tokens used: {total_tokens}" ) - if self._camel_has_request_usage: - total_tokens = 0 # Send deactivation for all non-streaming cases (success or error) # Streaming responses handle deactivation in _astream_chunks @@ -452,7 +438,9 @@ async def astep( "process_task_id": self.process_task_id, "agent_id": self.agent_id, "message": message, - "tokens": total_tokens, + "tokens": 0 + if self._camel_has_request_usage + else total_tokens, }, ) )