mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/agent: todo review nag as developer role, not fake user
Internal control uses DeveloperChatMessage so retry/history treat it as non-human. Responses bridge maps developer mid-turn; compact/history label it.
This commit is contained in:
@@ -8,6 +8,7 @@ from plyngent.lmproto.openai_compatible.model import (
|
||||
AssistantChatMessage,
|
||||
AssistantFunctionToolCall,
|
||||
ChatCompletionsParam,
|
||||
DeveloperChatMessage,
|
||||
SystemChatMessage,
|
||||
ToolChatMessage,
|
||||
UserChatMessage,
|
||||
@@ -43,12 +44,14 @@ _SEED_MESSAGE_TEMPLATE = (
|
||||
)
|
||||
|
||||
|
||||
def format_transcript(messages: Sequence[AnyChatMessage]) -> str:
|
||||
def format_transcript(messages: Sequence[AnyChatMessage]) -> str: # noqa: C901
|
||||
"""Render messages as plain text for a summarization prompt."""
|
||||
lines: list[str] = []
|
||||
for msg in messages:
|
||||
if isinstance(msg, SystemChatMessage):
|
||||
lines.append(f"[system] {msg.content}")
|
||||
elif isinstance(msg, DeveloperChatMessage):
|
||||
lines.append(f"[developer] {msg.content}")
|
||||
elif isinstance(msg, UserChatMessage):
|
||||
lines.append(f"[user] {msg.content}")
|
||||
elif isinstance(msg, AssistantChatMessage):
|
||||
|
||||
@@ -13,10 +13,10 @@ from plyngent.lmproto.openai_compatible.model import (
|
||||
AssistantChatMessage,
|
||||
AssistantFunctionToolCall,
|
||||
ChatCompletionsParam,
|
||||
DeveloperChatMessage,
|
||||
StreamOptions,
|
||||
StreamToolCallDelta,
|
||||
ToolChatMessage,
|
||||
UserChatMessage,
|
||||
)
|
||||
from plyngent.typedef import Unset # noqa: TC001
|
||||
|
||||
@@ -377,7 +377,8 @@ async def run_chat_loop(
|
||||
and not todo_review_injected
|
||||
):
|
||||
todo_review_injected = True
|
||||
messages.append(UserChatMessage(content=todo_stack.review_prompt()))
|
||||
# Non-user control identity so retry/history don't treat this as human input.
|
||||
messages.append(DeveloperChatMessage(content=todo_stack.review_prompt()))
|
||||
continue
|
||||
return
|
||||
async for event in _execute_tool_calls(
|
||||
|
||||
@@ -29,6 +29,7 @@ from plyngent.lmproto.openai_compatible.model import (
|
||||
ChatCompletionsParam,
|
||||
ChunkChoice,
|
||||
DeltaMessage,
|
||||
DeveloperChatMessage,
|
||||
StreamFunctionDelta,
|
||||
StreamToolCallDelta,
|
||||
SystemChatMessage,
|
||||
@@ -97,6 +98,10 @@ def chat_messages_to_responses_input(
|
||||
if isinstance(message, SystemChatMessage):
|
||||
if message.content.strip():
|
||||
instructions_parts.append(message.content)
|
||||
elif isinstance(message, DeveloperChatMessage):
|
||||
# Keep mid-turn control as input developer messages (not folded into instructions).
|
||||
if message.content.strip():
|
||||
items.append(ResponseEasyInputMessage(role="developer", content=message.content))
|
||||
elif isinstance(message, UserChatMessage):
|
||||
items.append(ResponseEasyInputMessage(role="user", content=message.content))
|
||||
elif isinstance(message, AssistantChatMessage):
|
||||
|
||||
@@ -99,9 +99,9 @@ class TodoStack:
|
||||
return "\n".join(lines)
|
||||
|
||||
def review_prompt(self) -> str:
|
||||
"""User-message body injected when the model finishes without todo ops."""
|
||||
"""Control-message body (developer role) when the model finishes without todo ops."""
|
||||
return (
|
||||
"[system: todo stack review]\n"
|
||||
"Todo stack review (internal control — not a human message).\n"
|
||||
"Open sub-tasks remain and you did not call any todo_* tools this turn.\n"
|
||||
"Review the stack: mark finished items done, update in_progress, "
|
||||
"push new sub-tasks, or pop/cancel obsolete ones. Then continue the user work.\n\n"
|
||||
|
||||
@@ -15,6 +15,8 @@ from plyngent.cli.selection import select_model, select_provider
|
||||
from plyngent.lmproto.openai_compatible.model import (
|
||||
AssistantChatMessage,
|
||||
AssistantFunctionToolCall,
|
||||
DeveloperChatMessage,
|
||||
SystemChatMessage,
|
||||
ToolChatMessage,
|
||||
UserChatMessage,
|
||||
)
|
||||
@@ -973,6 +975,10 @@ def _preview_content(text: str | None) -> str:
|
||||
def _format_history_message(index: int, message: AnyChatMessage) -> str:
|
||||
if isinstance(message, UserChatMessage):
|
||||
return f"{index}. user: {_preview_content(message.content)}"
|
||||
if isinstance(message, DeveloperChatMessage):
|
||||
return f"{index}. developer: {_preview_content(message.content)}"
|
||||
if isinstance(message, SystemChatMessage):
|
||||
return f"{index}. system: {_preview_content(message.content)}"
|
||||
if isinstance(message, AssistantChatMessage):
|
||||
parts: list[str] = []
|
||||
if isinstance(message.content, str) and message.content:
|
||||
|
||||
@@ -13,6 +13,7 @@ from plyngent.lmproto.openai_compatible.model import (
|
||||
ChatCompletionChunk,
|
||||
ChatCompletionResponse,
|
||||
ChatCompletionsParam,
|
||||
DeveloperChatMessage,
|
||||
UserChatMessage,
|
||||
)
|
||||
from plyngent.memory import MemoryStore
|
||||
@@ -102,7 +103,7 @@ class ScriptedClient:
|
||||
text = "ok" if self.calls > 1 else "done without todos"
|
||||
# Detect review message in history
|
||||
for msg in param.messages:
|
||||
if isinstance(msg, UserChatMessage) and "todo stack review" in msg.content:
|
||||
if isinstance(msg, DeveloperChatMessage) and "Todo stack review" in msg.content:
|
||||
text = "reviewed stack"
|
||||
break
|
||||
return ChatCompletionResponse(
|
||||
@@ -142,7 +143,11 @@ async def test_loop_injects_todo_review_when_untouched() -> None:
|
||||
pass
|
||||
assert client.calls >= 2
|
||||
assert any(
|
||||
isinstance(m, UserChatMessage) and "todo stack review" in m.content for m in agent.messages
|
||||
isinstance(m, DeveloperChatMessage) and "Todo stack review" in m.content
|
||||
for m in agent.messages
|
||||
)
|
||||
assert not any(
|
||||
isinstance(m, UserChatMessage) and "Todo stack review" in m.content for m in agent.messages
|
||||
)
|
||||
finally:
|
||||
set_todo_stack(None)
|
||||
|
||||
Reference in New Issue
Block a user