mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-25 08:04:57 +08:00
core/config: split agent system_prompt and tool_directives
This commit is contained in:
@@ -72,7 +72,7 @@ Async SQLAlchemy + aiosqlite. `MemoryStore`: schema init (+ lightweight SQLite `
|
|||||||
- **`/compact`**: soft-compact tool dumps → model summary (no tools) → **new** session seeded with summary message. Soft-compact keeps the last **12** tool results full-size by default (`DEFAULT_RECENT_TOOL_RESULTS`); older tool payloads shrink first.
|
- **`/compact`**: soft-compact tool dumps → model summary (no tools) → **new** session seeded with summary message. Soft-compact keeps the last **12** tool results full-size by default (`DEFAULT_RECENT_TOOL_RESULTS`); older tool payloads shrink first.
|
||||||
- Events: text_delta, **reasoning_delta**, assistant_message, tool_call/result, max_rounds, **error** (`retryable`/`source`), **cancelled** (`reason`), **usage** (`TokenUsage`).
|
- Events: text_delta, **reasoning_delta**, assistant_message, tool_call/result, max_rounds, **error** (`retryable`/`source`), **cancelled** (`reason`), **usage** (`TokenUsage`).
|
||||||
- Usage: API `usage` from completions (stream with `include_usage`); **char≈token fallback** (~4 chars/token) when omitted; **context size** = last request ``prompt_tokens`` (API preferred); `last_turn_usage` / `session_usage` are **billed sums** (tool rounds re-send history); CLI end-of-turn + `/status`.
|
- Usage: API `usage` from completions (stream with `include_usage`); **char≈token fallback** (~4 chars/token) when omitted; **context size** = last request ``prompt_tokens`` (API preferred); `last_turn_usage` / `session_usage` are **billed sums** (tool rounds re-send history); CLI end-of-turn + `/status`.
|
||||||
- Config ``[agent]``: `system_prompt` (default: built-in coding-agent guide `DEFAULT_SYSTEM_PROMPT`; `""` disables), `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_tokens` (default 200k est. tokens).
|
- Config ``[agent]``: `system_prompt` (persona; default `DEFAULT_SYSTEM_PROMPT`; `""` omits persona), `tool_directives` (tool playbook; default `DEFAULT_TOOL_DIRECTIVES`; `""` omits playbook; both empty → no system), `max_tool_result_chars`, `parallel_tools`, `confirm_destructive`, `path_denylist`, `max_context_tokens` (default 200k est. tokens).
|
||||||
|
|
||||||
### Tools (`tools/`)
|
### Tools (`tools/`)
|
||||||
|
|
||||||
|
|||||||
@@ -136,10 +136,11 @@ access_key_or_token = "sk-..."
|
|||||||
"gpt-4o-mini" = { text = true }
|
"gpt-4o-mini" = { text = true }
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
# system_prompt defaults to a built-in coding-agent guide (omit to use it).
|
# system_prompt = persona (omit → built-in). tool_directives = tool playbook.
|
||||||
# system_prompt = "" # disable
|
# system_prompt = "" / tool_directives = "" disable each part; both "" → no system.
|
||||||
# Or multi-line override (prefer '''...'''):
|
# Or multi-line override (prefer '''...'''):
|
||||||
# system_prompt = '''Your custom prompt...'''
|
# system_prompt = '''Your custom persona...'''
|
||||||
|
# tool_directives = '''### Workspace ...'''
|
||||||
confirm_destructive = true
|
confirm_destructive = true
|
||||||
max_context_tokens = 200000
|
max_context_tokens = 200000
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,12 +11,17 @@
|
|||||||
# # url = ":memory:"
|
# # url = ":memory:"
|
||||||
|
|
||||||
[agent]
|
[agent]
|
||||||
# system_prompt defaults to the built-in coding-agent guide when omitted.
|
# Persona (omit → built-in coding-agent line). tool_directives is the tool playbook.
|
||||||
# Override with a multi-line literal (prefer ''' so nested " is fine):
|
# Override with multi-line literals (prefer ''' so nested " is fine):
|
||||||
# system_prompt = '''
|
# system_prompt = '''
|
||||||
# Your custom system prompt...
|
# Your custom persona...
|
||||||
# '''
|
# '''
|
||||||
# system_prompt = "" # disable system prompt entirely
|
# tool_directives = '''
|
||||||
|
# ### Workspace
|
||||||
|
# ...
|
||||||
|
# '''
|
||||||
|
# system_prompt = "" # persona off; keep default tool_directives unless also cleared
|
||||||
|
# tool_directives = "" # playbook off (set both "" to disable system entirely)
|
||||||
max_tool_result_chars = 32000
|
max_tool_result_chars = 32000
|
||||||
parallel_tools = true
|
parallel_tools = true
|
||||||
confirm_destructive = true
|
confirm_destructive = true
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ _MINIMAL_CONFIG = """\
|
|||||||
# # url = ":memory:"
|
# # url = ":memory:"
|
||||||
|
|
||||||
# [agent]
|
# [agent]
|
||||||
# # system_prompt defaults to the built-in coding-agent guide when omitted.
|
# # system_prompt = persona (omit → built-in). tool_directives = tool playbook.
|
||||||
# # system_prompt = "" # disable; or multi-line '''...''' to override
|
# # system_prompt = "" / tool_directives = "" disable each part; both "" → no system.
|
||||||
|
# # Or multi-line '''...''' to override.
|
||||||
# max_tool_result_chars = 32000
|
# max_tool_result_chars = 32000
|
||||||
# parallel_tools = true
|
# parallel_tools = true
|
||||||
# confirm_destructive = true
|
# confirm_destructive = true
|
||||||
|
|||||||
@@ -169,9 +169,13 @@ class ReplState:
|
|||||||
|
|
||||||
def _make_agent(self) -> ChatAgent:
|
def _make_agent(self) -> ChatAgent:
|
||||||
from plyngent.cli.limits import prompt_continue_limit_async
|
from plyngent.cli.limits import prompt_continue_limit_async
|
||||||
|
from plyngent.config import compose_agent_system_content
|
||||||
|
|
||||||
agent_cfg = self.config.agent_config
|
agent_cfg = self.config.agent_config
|
||||||
system_prompt = agent_cfg.system_prompt or None
|
system_prompt = compose_agent_system_content(
|
||||||
|
agent_cfg.system_prompt,
|
||||||
|
agent_cfg.tool_directives,
|
||||||
|
)
|
||||||
on_limit = prompt_continue_limit_async if self.interactive_limits else None
|
on_limit = prompt_continue_limit_async if self.interactive_limits else None
|
||||||
return ChatAgent(
|
return ChatAgent(
|
||||||
self.client,
|
self.client,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import tomlkit
|
|||||||
from tomlkit.exceptions import TOMLKitError
|
from tomlkit.exceptions import TOMLKitError
|
||||||
|
|
||||||
from .models import DEFAULT_SYSTEM_PROMPT as DEFAULT_SYSTEM_PROMPT
|
from .models import DEFAULT_SYSTEM_PROMPT as DEFAULT_SYSTEM_PROMPT
|
||||||
|
from .models import DEFAULT_TOOL_DIRECTIVES as DEFAULT_TOOL_DIRECTIVES
|
||||||
from .models import AgentConfig as AgentConfig
|
from .models import AgentConfig as AgentConfig
|
||||||
from .models import AnthropicProvider as AnthropicProvider
|
from .models import AnthropicProvider as AnthropicProvider
|
||||||
from .models import DatabaseConfig as DatabaseConfig
|
from .models import DatabaseConfig as DatabaseConfig
|
||||||
@@ -15,6 +16,7 @@ from .models import ModelConfig as ModelConfig
|
|||||||
from .models import OpenAICompatibleProvider as OpenAICompatibleProvider
|
from .models import OpenAICompatibleProvider as OpenAICompatibleProvider
|
||||||
from .models import OpenAIProvider as OpenAIProvider
|
from .models import OpenAIProvider as OpenAIProvider
|
||||||
from .models import ProviderConfig as ProviderConfig
|
from .models import ProviderConfig as ProviderConfig
|
||||||
|
from .models import compose_agent_system_content as compose_agent_system_content
|
||||||
from .path import get_default_path as _get_default_path
|
from .path import get_default_path as _get_default_path
|
||||||
from .store import ConfigFormatError as ConfigFormatError
|
from .store import ConfigFormatError as ConfigFormatError
|
||||||
from .store import ConfigStore as ConfigStore
|
from .store import ConfigStore as ConfigStore
|
||||||
|
|||||||
@@ -2,12 +2,16 @@ from typing import Any
|
|||||||
|
|
||||||
from msgspec import Struct, field
|
from msgspec import Struct, field
|
||||||
|
|
||||||
# Built-in agent system prompt when ``[agent].system_prompt`` is omitted.
|
# Built-in persona when ``[agent].system_prompt`` is omitted.
|
||||||
# Set ``system_prompt = ""`` in TOML to disable. Override with a multi-line
|
# Set ``system_prompt = ""`` to omit the persona block only.
|
||||||
# TOML literal (prefer '''...''' so nested " quotes are fine).
|
# Override with a multi-line TOML literal (prefer ''' so nested " is fine).
|
||||||
DEFAULT_SYSTEM_PROMPT = """\
|
DEFAULT_SYSTEM_PROMPT = """\
|
||||||
You are a professional coding agent in a workspace-bound tool environment.
|
You are a professional coding agent in a workspace-bound tool environment.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Built-in tool playbook when ``[agent].tool_directives`` is omitted.
|
||||||
|
# Set ``tool_directives = ""`` to omit this block only.
|
||||||
|
DEFAULT_TOOL_DIRECTIVES = """\
|
||||||
### Workspace
|
### Workspace
|
||||||
- Explore with `tree`/`listdir`/`glob_paths`/`grep_files`/`read_file` when unsure.
|
- Explore with `tree`/`listdir`/`glob_paths`/`grep_files`/`read_file` when unsure.
|
||||||
- Stay under the workspace (or a path from `new_temporary_workspace`). Respect path denylists.
|
- Stay under the workspace (or a path from `new_temporary_workspace`). Respect path denylists.
|
||||||
@@ -41,6 +45,21 @@ pop the group). Open items mean unfinished work.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def compose_agent_system_content(
|
||||||
|
system_prompt: str,
|
||||||
|
tool_directives: str,
|
||||||
|
) -> str | None:
|
||||||
|
"""Join persona + tool playbook into one system body, or ``None`` if both empty.
|
||||||
|
|
||||||
|
Non-empty parts are stripped and joined with a blank line. Either field may
|
||||||
|
be ``""`` to disable that part while keeping the other.
|
||||||
|
"""
|
||||||
|
parts = [part.strip() for part in (system_prompt, tool_directives) if part and part.strip()]
|
||||||
|
if not parts:
|
||||||
|
return None
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
|
||||||
|
|
||||||
class DatabaseConfig(Struct, omit_defaults=True):
|
class DatabaseConfig(Struct, omit_defaults=True):
|
||||||
"""Database connection configuration.
|
"""Database connection configuration.
|
||||||
|
|
||||||
@@ -58,7 +77,10 @@ class DatabaseConfig(Struct, omit_defaults=True):
|
|||||||
class AgentConfig(Struct, omit_defaults=True):
|
class AgentConfig(Struct, omit_defaults=True):
|
||||||
"""Single-user agent profile defaults."""
|
"""Single-user agent profile defaults."""
|
||||||
|
|
||||||
|
# Persona / role (omit → DEFAULT_SYSTEM_PROMPT; "" disables persona only).
|
||||||
system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
system_prompt: str = DEFAULT_SYSTEM_PROMPT
|
||||||
|
# Tool how-to (omit → DEFAULT_TOOL_DIRECTIVES; "" disables playbook only).
|
||||||
|
tool_directives: str = DEFAULT_TOOL_DIRECTIVES
|
||||||
max_tool_result_chars: int = 32_000
|
max_tool_result_chars: int = 32_000
|
||||||
parallel_tools: bool = True
|
parallel_tools: bool = True
|
||||||
confirm_destructive: bool = True
|
confirm_destructive: bool = True
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from plyngent.config import DEFAULT_SYSTEM_PROMPT, load
|
from plyngent.config import (
|
||||||
|
DEFAULT_SYSTEM_PROMPT,
|
||||||
|
DEFAULT_TOOL_DIRECTIVES,
|
||||||
|
compose_agent_system_content,
|
||||||
|
load,
|
||||||
|
)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -13,7 +18,9 @@ def test_agent_section_defaults(tmp_path: Path) -> None:
|
|||||||
_ = path.write_text("", encoding="utf-8")
|
_ = path.write_text("", encoding="utf-8")
|
||||||
store = load(path)
|
store = load(path)
|
||||||
assert store.agent_config.system_prompt == DEFAULT_SYSTEM_PROMPT
|
assert store.agent_config.system_prompt == DEFAULT_SYSTEM_PROMPT
|
||||||
|
assert store.agent_config.tool_directives == DEFAULT_TOOL_DIRECTIVES
|
||||||
assert "professional coding agent" in store.agent_config.system_prompt
|
assert "professional coding agent" in store.agent_config.system_prompt
|
||||||
|
assert "### Workspace" in store.agent_config.tool_directives
|
||||||
assert store.agent_config.max_tool_result_chars == 32_000
|
assert store.agent_config.max_tool_result_chars == 32_000
|
||||||
assert store.agent_config.parallel_tools is True
|
assert store.agent_config.parallel_tools is True
|
||||||
assert store.agent_config.confirm_destructive is True
|
assert store.agent_config.confirm_destructive is True
|
||||||
@@ -21,12 +28,31 @@ def test_agent_section_defaults(tmp_path: Path) -> None:
|
|||||||
assert store.agent_config.max_context_tokens == 200_000
|
assert store.agent_config.max_context_tokens == 200_000
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_defaults_join_persona_and_directives() -> None:
|
||||||
|
body = compose_agent_system_content(DEFAULT_SYSTEM_PROMPT, DEFAULT_TOOL_DIRECTIVES)
|
||||||
|
assert body is not None
|
||||||
|
assert body.startswith("You are a professional coding agent")
|
||||||
|
assert "\n\n### Workspace" in body
|
||||||
|
assert "### Todos" in body
|
||||||
|
# No double-blank-line collapse issues between parts.
|
||||||
|
assert "\n\n\n" not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_compose_empty_combinations() -> None:
|
||||||
|
assert compose_agent_system_content("", "") is None
|
||||||
|
assert compose_agent_system_content(" ", "\n") is None
|
||||||
|
assert compose_agent_system_content("Only persona", "") == "Only persona"
|
||||||
|
assert compose_agent_system_content("", "Only tools") == "Only tools"
|
||||||
|
assert compose_agent_system_content("Persona", "Tools") == "Persona\n\nTools"
|
||||||
|
|
||||||
|
|
||||||
def test_agent_section_parse(tmp_path: Path) -> None:
|
def test_agent_section_parse(tmp_path: Path) -> None:
|
||||||
path = tmp_path / "c.toml"
|
path = tmp_path / "c.toml"
|
||||||
_ = path.write_text(
|
_ = path.write_text(
|
||||||
"""
|
"""
|
||||||
[agent]
|
[agent]
|
||||||
system_prompt = "Be brief."
|
system_prompt = "Be brief."
|
||||||
|
tool_directives = "Use tools carefully."
|
||||||
max_tool_result_chars = 100
|
max_tool_result_chars = 100
|
||||||
parallel_tools = false
|
parallel_tools = false
|
||||||
confirm_destructive = false
|
confirm_destructive = false
|
||||||
@@ -37,14 +63,20 @@ max_context_tokens = 5000
|
|||||||
)
|
)
|
||||||
store = load(path)
|
store = load(path)
|
||||||
assert store.agent_config.system_prompt == "Be brief."
|
assert store.agent_config.system_prompt == "Be brief."
|
||||||
|
assert store.agent_config.tool_directives == "Use tools carefully."
|
||||||
assert store.agent_config.max_tool_result_chars == 100
|
assert store.agent_config.max_tool_result_chars == 100
|
||||||
assert store.agent_config.parallel_tools is False
|
assert store.agent_config.parallel_tools is False
|
||||||
assert store.agent_config.confirm_destructive is False
|
assert store.agent_config.confirm_destructive is False
|
||||||
assert store.agent_config.path_denylist == ["/secrets/", ".ssh/"]
|
assert store.agent_config.path_denylist == ["/secrets/", ".ssh/"]
|
||||||
assert store.agent_config.max_context_tokens == 5000
|
assert store.agent_config.max_context_tokens == 5000
|
||||||
|
composed = compose_agent_system_content(
|
||||||
|
store.agent_config.system_prompt,
|
||||||
|
store.agent_config.tool_directives,
|
||||||
|
)
|
||||||
|
assert composed == "Be brief.\n\nUse tools carefully."
|
||||||
|
|
||||||
|
|
||||||
def test_agent_system_prompt_empty_disables(tmp_path: Path) -> None:
|
def test_agent_system_prompt_empty_disables_persona_only(tmp_path: Path) -> None:
|
||||||
path = tmp_path / "c.toml"
|
path = tmp_path / "c.toml"
|
||||||
_ = path.write_text(
|
_ = path.write_text(
|
||||||
"""
|
"""
|
||||||
@@ -55,3 +87,52 @@ system_prompt = ""
|
|||||||
)
|
)
|
||||||
store = load(path)
|
store = load(path)
|
||||||
assert store.agent_config.system_prompt == ""
|
assert store.agent_config.system_prompt == ""
|
||||||
|
assert store.agent_config.tool_directives == DEFAULT_TOOL_DIRECTIVES
|
||||||
|
body = compose_agent_system_content(
|
||||||
|
store.agent_config.system_prompt,
|
||||||
|
store.agent_config.tool_directives,
|
||||||
|
)
|
||||||
|
assert body is not None
|
||||||
|
assert "### Workspace" in body
|
||||||
|
assert "professional coding agent" not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_tool_directives_empty_disables_playbook_only(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "c.toml"
|
||||||
|
_ = path.write_text(
|
||||||
|
"""
|
||||||
|
[agent]
|
||||||
|
tool_directives = ""
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
store = load(path)
|
||||||
|
assert store.agent_config.tool_directives == ""
|
||||||
|
assert store.agent_config.system_prompt == DEFAULT_SYSTEM_PROMPT
|
||||||
|
body = compose_agent_system_content(
|
||||||
|
store.agent_config.system_prompt,
|
||||||
|
store.agent_config.tool_directives,
|
||||||
|
)
|
||||||
|
assert body is not None
|
||||||
|
assert "professional coding agent" in body
|
||||||
|
assert "### Workspace" not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_agent_both_empty_disables_system(tmp_path: Path) -> None:
|
||||||
|
path = tmp_path / "c.toml"
|
||||||
|
_ = path.write_text(
|
||||||
|
"""
|
||||||
|
[agent]
|
||||||
|
system_prompt = ""
|
||||||
|
tool_directives = ""
|
||||||
|
""",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
store = load(path)
|
||||||
|
assert (
|
||||||
|
compose_agent_system_content(
|
||||||
|
store.agent_config.system_prompt,
|
||||||
|
store.agent_config.tool_directives,
|
||||||
|
)
|
||||||
|
is None
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user