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,
|
AssistantChatMessage,
|
||||||
AssistantFunctionToolCall,
|
AssistantFunctionToolCall,
|
||||||
ChatCompletionsParam,
|
ChatCompletionsParam,
|
||||||
|
DeveloperChatMessage,
|
||||||
SystemChatMessage,
|
SystemChatMessage,
|
||||||
ToolChatMessage,
|
ToolChatMessage,
|
||||||
UserChatMessage,
|
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."""
|
"""Render messages as plain text for a summarization prompt."""
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if isinstance(msg, SystemChatMessage):
|
if isinstance(msg, SystemChatMessage):
|
||||||
lines.append(f"[system] {msg.content}")
|
lines.append(f"[system] {msg.content}")
|
||||||
|
elif isinstance(msg, DeveloperChatMessage):
|
||||||
|
lines.append(f"[developer] {msg.content}")
|
||||||
elif isinstance(msg, UserChatMessage):
|
elif isinstance(msg, UserChatMessage):
|
||||||
lines.append(f"[user] {msg.content}")
|
lines.append(f"[user] {msg.content}")
|
||||||
elif isinstance(msg, AssistantChatMessage):
|
elif isinstance(msg, AssistantChatMessage):
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ from plyngent.lmproto.openai_compatible.model import (
|
|||||||
AssistantChatMessage,
|
AssistantChatMessage,
|
||||||
AssistantFunctionToolCall,
|
AssistantFunctionToolCall,
|
||||||
ChatCompletionsParam,
|
ChatCompletionsParam,
|
||||||
|
DeveloperChatMessage,
|
||||||
StreamOptions,
|
StreamOptions,
|
||||||
StreamToolCallDelta,
|
StreamToolCallDelta,
|
||||||
ToolChatMessage,
|
ToolChatMessage,
|
||||||
UserChatMessage,
|
|
||||||
)
|
)
|
||||||
from plyngent.typedef import Unset # noqa: TC001
|
from plyngent.typedef import Unset # noqa: TC001
|
||||||
|
|
||||||
@@ -377,7 +377,8 @@ async def run_chat_loop(
|
|||||||
and not todo_review_injected
|
and not todo_review_injected
|
||||||
):
|
):
|
||||||
todo_review_injected = True
|
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
|
continue
|
||||||
return
|
return
|
||||||
async for event in _execute_tool_calls(
|
async for event in _execute_tool_calls(
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ from plyngent.lmproto.openai_compatible.model import (
|
|||||||
ChatCompletionsParam,
|
ChatCompletionsParam,
|
||||||
ChunkChoice,
|
ChunkChoice,
|
||||||
DeltaMessage,
|
DeltaMessage,
|
||||||
|
DeveloperChatMessage,
|
||||||
StreamFunctionDelta,
|
StreamFunctionDelta,
|
||||||
StreamToolCallDelta,
|
StreamToolCallDelta,
|
||||||
SystemChatMessage,
|
SystemChatMessage,
|
||||||
@@ -97,6 +98,10 @@ def chat_messages_to_responses_input(
|
|||||||
if isinstance(message, SystemChatMessage):
|
if isinstance(message, SystemChatMessage):
|
||||||
if message.content.strip():
|
if message.content.strip():
|
||||||
instructions_parts.append(message.content)
|
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):
|
elif isinstance(message, UserChatMessage):
|
||||||
items.append(ResponseEasyInputMessage(role="user", content=message.content))
|
items.append(ResponseEasyInputMessage(role="user", content=message.content))
|
||||||
elif isinstance(message, AssistantChatMessage):
|
elif isinstance(message, AssistantChatMessage):
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ class TodoStack:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
def review_prompt(self) -> str:
|
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 (
|
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"
|
"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, "
|
"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"
|
"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 (
|
from plyngent.lmproto.openai_compatible.model import (
|
||||||
AssistantChatMessage,
|
AssistantChatMessage,
|
||||||
AssistantFunctionToolCall,
|
AssistantFunctionToolCall,
|
||||||
|
DeveloperChatMessage,
|
||||||
|
SystemChatMessage,
|
||||||
ToolChatMessage,
|
ToolChatMessage,
|
||||||
UserChatMessage,
|
UserChatMessage,
|
||||||
)
|
)
|
||||||
@@ -973,6 +975,10 @@ def _preview_content(text: str | None) -> str:
|
|||||||
def _format_history_message(index: int, message: AnyChatMessage) -> str:
|
def _format_history_message(index: int, message: AnyChatMessage) -> str:
|
||||||
if isinstance(message, UserChatMessage):
|
if isinstance(message, UserChatMessage):
|
||||||
return f"{index}. user: {_preview_content(message.content)}"
|
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):
|
if isinstance(message, AssistantChatMessage):
|
||||||
parts: list[str] = []
|
parts: list[str] = []
|
||||||
if isinstance(message.content, str) and message.content:
|
if isinstance(message.content, str) and message.content:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from plyngent.lmproto.openai_compatible.model import (
|
|||||||
ChatCompletionChunk,
|
ChatCompletionChunk,
|
||||||
ChatCompletionResponse,
|
ChatCompletionResponse,
|
||||||
ChatCompletionsParam,
|
ChatCompletionsParam,
|
||||||
|
DeveloperChatMessage,
|
||||||
UserChatMessage,
|
UserChatMessage,
|
||||||
)
|
)
|
||||||
from plyngent.memory import MemoryStore
|
from plyngent.memory import MemoryStore
|
||||||
@@ -102,7 +103,7 @@ class ScriptedClient:
|
|||||||
text = "ok" if self.calls > 1 else "done without todos"
|
text = "ok" if self.calls > 1 else "done without todos"
|
||||||
# Detect review message in history
|
# Detect review message in history
|
||||||
for msg in param.messages:
|
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"
|
text = "reviewed stack"
|
||||||
break
|
break
|
||||||
return ChatCompletionResponse(
|
return ChatCompletionResponse(
|
||||||
@@ -142,7 +143,11 @@ async def test_loop_injects_todo_review_when_untouched() -> None:
|
|||||||
pass
|
pass
|
||||||
assert client.calls >= 2
|
assert client.calls >= 2
|
||||||
assert any(
|
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:
|
finally:
|
||||||
set_todo_stack(None)
|
set_todo_stack(None)
|
||||||
|
|||||||
Reference in New Issue
Block a user