mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 14:14:57 +08:00
core/agent: derive pending_retry_text from trailing user message
Drop stored pending field; incomplete turns are just history ending in UserChatMessage. Compact seeds use an assistant summary so they are not mistaken for retryable orphans.
This commit is contained in:
+19
-41
@@ -43,7 +43,6 @@ class ChatAgent:
|
||||
parallel_tools: bool
|
||||
max_context_tokens: int
|
||||
messages: list[AnyChatMessage]
|
||||
pending_retry_text: str | None
|
||||
session_usage: TokenUsage
|
||||
last_turn_usage: TokenUsage
|
||||
last_request_usage: TokenUsage
|
||||
@@ -81,13 +80,22 @@ class ChatAgent:
|
||||
self.parallel_tools = parallel_tools
|
||||
self.max_context_tokens = max_context_tokens
|
||||
self.messages = list(messages) if messages is not None else []
|
||||
self.pending_retry_text = None
|
||||
self.session_usage = TokenUsage()
|
||||
self.last_turn_usage = TokenUsage()
|
||||
self.last_request_usage = TokenUsage()
|
||||
self.last_turn_rounds = 0
|
||||
self._ensure_system_prompt()
|
||||
self._sync_pending_from_orphan_user()
|
||||
|
||||
@property
|
||||
def pending_retry_text(self) -> str | None:
|
||||
"""Text of an incomplete last user turn, if any.
|
||||
|
||||
Derived only: history ending with a user message means that turn never
|
||||
completed (failure, cancel, or resume of an orphan user in DB).
|
||||
"""
|
||||
if self.messages and isinstance(self.messages[-1], UserChatMessage):
|
||||
return self.messages[-1].content
|
||||
return None
|
||||
|
||||
@property
|
||||
def context_tokens(self) -> int:
|
||||
@@ -116,13 +124,6 @@ class ChatAgent:
|
||||
return
|
||||
self.messages.insert(0, SystemChatMessage(content=self.system_prompt))
|
||||
|
||||
def _sync_pending_from_orphan_user(self) -> None:
|
||||
"""If history ends with a user message, that turn is incomplete → retryable."""
|
||||
if self.messages and isinstance(self.messages[-1], UserChatMessage):
|
||||
self.pending_retry_text = self.messages[-1].content
|
||||
else:
|
||||
self.pending_retry_text = None
|
||||
|
||||
async def load_history(self) -> None:
|
||||
"""Replace in-memory messages from the bound memory session."""
|
||||
if self.memory is None or self.session_id is None:
|
||||
@@ -130,7 +131,6 @@ class ChatAgent:
|
||||
raise RuntimeError(msg)
|
||||
self.messages = await self.memory.list_messages(self.session_id)
|
||||
self._ensure_system_prompt()
|
||||
self._sync_pending_from_orphan_user()
|
||||
|
||||
async def bind_session(self, session_id: int, *, load: bool = True) -> None:
|
||||
"""Attach a memory session id; optionally load existing messages."""
|
||||
@@ -149,7 +149,6 @@ class ChatAgent:
|
||||
for i in range(len(self.messages) - 1, -1, -1):
|
||||
if self.messages[i] is user_msg:
|
||||
return i
|
||||
# Fallback: last matching content user message
|
||||
for i in range(len(self.messages) - 1, -1, -1):
|
||||
msg = self.messages[i]
|
||||
if isinstance(msg, UserChatMessage) and msg.content == user_msg.content:
|
||||
@@ -157,17 +156,16 @@ class ChatAgent:
|
||||
msg = "user message not found in history"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
def _rollback_partial(self, user_index: int, user_text: str) -> None:
|
||||
def _rollback_partial(self, user_index: int) -> None:
|
||||
"""Drop assistant/tool messages after the user; keep user for retry/DB."""
|
||||
del self.messages[user_index + 1 :]
|
||||
self.pending_retry_text = user_text
|
||||
|
||||
async def _run_from_user_message(self, user_msg: UserChatMessage) -> AsyncIterator[AgentEvent]:
|
||||
"""Run the tool loop for an already-appended user message.
|
||||
|
||||
On success, persists assistant/tool messages produced after the user.
|
||||
On failure, keeps the user message (already in memory/DB) and sets
|
||||
``pending_retry_text`` so ``retry()`` can re-run without duplicating it.
|
||||
On failure, keeps the trailing user message so :meth:`retry` can re-run
|
||||
without duplicating it (see :attr:`pending_retry_text`).
|
||||
"""
|
||||
user_index = self._user_index(user_msg)
|
||||
|
||||
@@ -190,7 +188,6 @@ class ChatAgent:
|
||||
max_context_tokens=self.max_context_tokens,
|
||||
):
|
||||
if isinstance(event, UsageEvent):
|
||||
# Each tool-loop round re-sends history; sum is billing, not context size.
|
||||
turn_rounds += 1
|
||||
last_request = event.usage
|
||||
turn_usage = turn_usage.add(event.usage)
|
||||
@@ -199,7 +196,7 @@ class ChatAgent:
|
||||
completed = True
|
||||
except BaseException:
|
||||
if not completed:
|
||||
self._rollback_partial(user_index, user_msg.content)
|
||||
self._rollback_partial(user_index)
|
||||
raise
|
||||
|
||||
self.last_turn_usage = turn_usage
|
||||
@@ -207,7 +204,6 @@ class ChatAgent:
|
||||
self.last_turn_rounds = turn_rounds
|
||||
for message in self.messages[user_index + 1 :]:
|
||||
await self._persist(message)
|
||||
self.pending_retry_text = None
|
||||
|
||||
async def run(self, user_text: str) -> AsyncIterator[AgentEvent]:
|
||||
"""Append a user message (persist immediately), run the tool loop, yield events."""
|
||||
@@ -221,31 +217,13 @@ class ChatAgent:
|
||||
async def retry(self) -> AsyncIterator[AgentEvent]:
|
||||
"""Re-run the incomplete last user turn without appending a new user message.
|
||||
|
||||
Useful after a failed/cancelled turn (user already in memory/DB) or after
|
||||
``load_history`` when the session ends with an orphan user message.
|
||||
Requires history to end with a :class:`UserChatMessage` (failed/cancelled
|
||||
turn or resumed orphan user in the session DB).
|
||||
"""
|
||||
text = self.pending_retry_text
|
||||
if text is None:
|
||||
self._sync_pending_from_orphan_user()
|
||||
text = self.pending_retry_text
|
||||
if text is None:
|
||||
if not self.messages or not isinstance(self.messages[-1], UserChatMessage):
|
||||
msg = "nothing to retry"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
user_msg = self.messages[-1]
|
||||
self._ensure_system_prompt()
|
||||
if self.messages and isinstance(self.messages[-1], UserChatMessage):
|
||||
user_msg = self.messages[-1]
|
||||
if user_msg.content != text:
|
||||
# Pending text out of sync: replace trailing user
|
||||
user_msg = UserChatMessage(content=text)
|
||||
self.messages[-1] = user_msg
|
||||
else:
|
||||
user_msg = UserChatMessage(content=text)
|
||||
self.messages.append(user_msg)
|
||||
# Already persisted on the original run when memory is bound; only
|
||||
# persist if this is a reconstructed orphan without DB (no memory).
|
||||
if self.memory is None or self.session_id is None:
|
||||
await self._persist(user_msg)
|
||||
|
||||
async for event in self._run_from_user_message(user_msg):
|
||||
yield event
|
||||
|
||||
Reference in New Issue
Block a user