Skip to content

Commit f4181c8

Browse files
authored
Merge pull request #85 from vstorm-co/issue/84
Issue/84
2 parents b7eae9a + 82cd755 commit f4181c8

11 files changed

Lines changed: 119 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [0.3.16] - 2026-04-22
9+
10+
### Changed
11+
12+
- **`instructions` now replaces `BASE_PROMPT` instead of appending to it** — previously, passing `instructions="..."` to `create_deep_agent()` produced a system prompt of `BASE_PROMPT + "\n\n" + instructions`. Now `instructions` is used verbatim as the full system prompt. `instructions=None` (the default) keeps the existing behaviour — `BASE_PROMPT` is used automatically. Users who want to extend the default rather than replace it can do so with an f-string: `instructions=f"{BASE_PROMPT}\n\nYour extra text"`. `BASE_PROMPT` is exported from the top-level package. ([#84](https://github.com/vstorm-co/pydantic-deepagents/issues/84), reported by [@rremilian](https://github.com/rremilian))
13+
- **Subagent and team-member factories always prepend `BASE_PROMPT`** — agents spawned automatically by the `task()` tool or `spawn_team()` continue to receive `BASE_PROMPT` followed by their task-specific `instructions`, so subagent behaviour is unchanged.
14+
- **`apps/deepresearch` double-prompt bug fixed**`MAIN_INSTRUCTIONS` already contained `BASE_PROMPT`; it was previously duplicated in the final system prompt because the old append logic prepended it again. The new semantics resolve this without any change to the deepresearch app itself.
15+
816
## [0.3.15] - 2026-04-17
917

1018
### Fixed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636

3737
## What's New
3838

39+
- **2026-04-22**  **v0.3.16**`instructions=` now replaces `BASE_PROMPT` directly. Use `f"{BASE_PROMPT}\n\n..."` to extend it. Subagents still get `BASE_PROMPT` automatically.
3940
- **2026-04-12**  **v0.3.8** — Stuck loop detection, context limit warnings for the model, expanded context file discovery (CLAUDE.md, .cursorrules, etc.), eviction & orphan repair migrated to capabilities hooks.
4041
- **2026-04-11**  **v0.3.6** — One-command installer + self-update: `curl -fsSL .../install.sh | bash` installs everything automatically. New `pydantic-deep update` command. Startup update notifications with 24-hour PyPI cache.
4142
- **2026-04-10**  **v0.3.5** — Headless runner (`pydantic-deep run`), Docker sandbox with named workspaces, browser automation via Playwright, Harbor adapter for Terminal Bench evaluation.

apps/cli/agent.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pydantic_ai_backends import LocalBackend
99

1010
from apps.cli.prompts import build_cli_instructions
11-
from pydantic_deep.agent import create_deep_agent
11+
from pydantic_deep.agent import DEFAULT_INSTRUCTIONS, create_deep_agent
1212
from pydantic_deep.capabilities.hooks import Hook, HookEvent, HookInput, HookResult
1313
from pydantic_deep.deps import DeepAgentDeps
1414

@@ -170,10 +170,13 @@ def create_cli_agent( # noqa: C901
170170
if extra_middleware:
171171
middleware.extend(extra_middleware)
172172

173-
# Build dynamic system prompt (tool-specific guidance lives in tool descriptions)
174-
instructions = build_cli_instructions(
175-
non_interactive=non_interactive,
176-
lean=lean,
173+
instructions = (
174+
DEFAULT_INSTRUCTIONS
175+
+ "\n\n"
176+
+ build_cli_instructions(
177+
non_interactive=non_interactive,
178+
lean=lean,
179+
)
177180
)
178181

179182
# Append working directory context

apps/cli/prompts.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,6 @@ def build_cli_instructions(
203203
if non_interactive and lean:
204204
return _LEAN_NON_INTERACTIVE
205205

206-
# NOTE: BASE_PROMPT is prepended by create_deep_agent() — do NOT include it here.
207-
# CLI prompt only adds CLI-specific sections on top of the framework base.
208206
parts: list[str] = [_CLI_CORE, _CODE_QUALITY_SECTION]
209207

210208
if non_interactive:

docs/advanced/subagents.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ task(
7272
This:
7373

7474
1. Creates a **deep agent** (`create_deep_agent()`) with:
75-
- `BASE_PROMPT` + subagent's `instructions` as system prompt
75+
- `BASE_PROMPT` prepended to the subagent's `instructions` as system prompt
7676
- Filesystem, web, todo, memory tools
7777
- Eviction and patch tool calls support
7878
2. Clones dependencies with:

docs/concepts/agents.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ agent = create_deep_agent(model=TestModel())
3333

3434
### Custom Instructions
3535

36+
The `instructions` parameter sets the agent's system prompt. When provided, it **replaces** the built-in [`BASE_PROMPT`][pydantic_deep.prompts.BASE_PROMPT] entirely:
37+
3638
```python
3739
agent = create_deep_agent(
3840
instructions="""
@@ -46,6 +48,24 @@ agent = create_deep_agent(
4648
)
4749
```
4850

51+
To build on top of the default behavior instead of replacing it, import `BASE_PROMPT` and compose with an f-string:
52+
53+
```python
54+
from pydantic_deep import BASE_PROMPT, create_deep_agent
55+
56+
agent = create_deep_agent(
57+
instructions=f"""{BASE_PROMPT}
58+
59+
## Extra Guidelines
60+
61+
You are a Python expert. Always use type hints and docstrings.
62+
"""
63+
)
64+
```
65+
66+
!!! note "Subagent instructions work differently"
67+
The `instructions` field in [`SubAgentConfig`][pydantic_deep.types.SubAgentConfig] is always **appended** to `BASE_PROMPT` automatically — you only write the specialized part. This keeps subagent configs concise.
68+
4969
### Enabling/Disabling Features
5070

5171
```python
@@ -321,15 +341,15 @@ Pydantic Deep Agents uses a dynamic system prompt mechanism that automatically c
321341
```python
322342
# The agent automatically includes relevant prompts based on enabled features
323343
agent = create_deep_agent(
324-
instructions="You are a Python expert.", # Your base instructions
344+
instructions="You are a Python expert.", # Replaces BASE_PROMPT
325345
include_todo=True, # Adds todo prompt
326346
include_filesystem=True, # Adds console prompt
327347
include_subagents=True, # Adds subagent prompt
328348
include_skills=True, # Adds skills prompt
329349
)
330350

331351
# At runtime, the agent sees:
332-
# 1. Your instructions: "You are a Python expert."
352+
# 1. Static instructions: "You are a Python expert." (or BASE_PROMPT if instructions=None)
333353
# 2. Uploaded files: "## Uploaded Files\n- /uploads/data.csv (1024 bytes, 50 lines)"
334354
# 3. Todo prompt: "## Current Todos\n- [ ] Analyze data..."
335355
# 4. Console prompt: "## File Operations\nYou can use ls, read_file, write_file..."

docs/examples/basic-usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ Usage Statistics:
158158
```python
159159
agent = create_deep_agent(
160160
model="anthropic:claude-sonnet-4-6", # LLM model
161-
instructions="...", # System prompt
161+
instructions="...", # System prompt (replaces built-in BASE_PROMPT)
162162
)
163163
```
164164

pydantic_deep/agent.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def create_deep_agent(
7070
model: str | Model | None = None,
7171
model_settings: dict[str, Any] | None = None,
7272
summarization_model: str | None = None,
73+
base_prompt: str | None = None,
7374
instructions: str | None = None,
7475
output_style: str | Any | None = None,
7576
styles_dir: str | list[str] | None = None,
@@ -137,6 +138,7 @@ def create_deep_agent(
137138
model: str | Model | None = None,
138139
model_settings: dict[str, Any] | None = None,
139140
summarization_model: str | None = None,
141+
base_prompt: str | None = None,
140142
instructions: str | None = None,
141143
output_style: str | Any | None = None,
142144
styles_dir: str | list[str] | None = None,
@@ -204,6 +206,7 @@ def create_deep_agent( # noqa: C901
204206
model: str | Model | None = None,
205207
model_settings: dict[str, Any] | None = None,
206208
summarization_model: str | None = None,
209+
base_prompt: str | None = None,
207210
instructions: str | None = None,
208211
output_style: str | Any | None = None,
209212
styles_dir: str | list[str] | None = None,
@@ -277,7 +280,10 @@ def create_deep_agent( # noqa: C901
277280
278281
Args:
279282
model: Model to use (default: anthropic:claude-opus-4-6).
280-
instructions: Custom instructions for the agent.
283+
instructions: System prompt for the agent. When provided, replaces the
284+
default ``BASE_PROMPT`` entirely. Use ``BASE_PROMPT`` from
285+
``pydantic_deep`` to build on top of it:
286+
``instructions=f"{BASE_PROMPT}\\n\\nYour extra instructions"``.
281287
output_style: Output style to apply to agent responses. Can be a
282288
string name of a built-in style ("concise", "explanatory",
283289
"formal", "conversational"), a custom OutputStyle instance,
@@ -564,9 +570,15 @@ def _set_toolset_retries(toolset: AbstractToolset[DeepAgentDeps], max_retries: i
564570

565571
def _default_deep_agent_factory(cfg: dict[str, Any]) -> Any:
566572
"""Create a deep agent for subagent execution."""
573+
_sub_task_instructions = cfg.get("instructions") or ""
574+
_sub_instructions = (
575+
DEFAULT_INSTRUCTIONS + "\n\n" + _sub_task_instructions
576+
if _sub_task_instructions
577+
else DEFAULT_INSTRUCTIONS
578+
)
567579
return create_deep_agent(
568580
model=cfg.get("model", _sub_model),
569-
instructions=cfg["instructions"],
581+
instructions=_sub_instructions,
570582
include_filesystem=True,
571583
include_execute=True,
572584
include_todo=True,
@@ -732,9 +744,15 @@ def _default_deep_agent_factory(cfg: dict[str, Any]) -> Any:
732744
_team_edit_fmt = edit_format
733745

734746
def _deep_agent_factory(cfg: dict[str, Any]) -> Any: # pragma: no cover
747+
_team_task_instructions = cfg.get("instructions") or ""
748+
_team_instructions = (
749+
DEFAULT_INSTRUCTIONS + "\n\n" + _team_task_instructions
750+
if _team_task_instructions
751+
else DEFAULT_INSTRUCTIONS
752+
)
735753
return create_deep_agent(
736754
model=cfg.get("model", _team_model),
737-
instructions=cfg["instructions"],
755+
instructions=_team_instructions,
738756
include_filesystem=True,
739757
include_todo=True,
740758
include_subagents=False,
@@ -751,10 +769,7 @@ def _deep_agent_factory(cfg: dict[str, Any]) -> Any: # pragma: no cover
751769
team_toolset = create_team_toolset(**_team_kwargs)
752770
all_toolsets.append(team_toolset)
753771

754-
# Build base instructions — always include BASE_PROMPT, append user instructions
755-
base_instructions = DEFAULT_INSTRUCTIONS
756-
if instructions:
757-
base_instructions = base_instructions + "\n\n" + instructions
772+
base_instructions = instructions if instructions is not None else DEFAULT_INSTRUCTIONS
758773

759774
# Improve toolset (self-improvement from session analysis)
760775
if include_improve:

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pydantic-deep"
3-
version = "0.3.15"
3+
version = "0.3.16"
44
description = "Batteries-included agent harness for Python — tool-calling, sandboxed execution, multi-agent teams, and unlimited context on Pydantic AI"
55
readme = "README.md"
66
keywords = [

tests/test_agent.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,41 @@ def _sub_caps(parent_web_search: bool, parent_web_fetch: bool) -> list[type]:
9999
assert WebSearch in on_types
100100
assert WebFetch in on_types
101101

102+
def test_default_subagent_factory_prepends_base_prompt(self):
103+
"""Subagent factory always prepends BASE_PROMPT before task instructions."""
104+
from pydantic_deep.prompts import BASE_PROMPT
105+
106+
subagents: list[SubAgentConfig] = [
107+
SubAgentConfig(
108+
name="researcher",
109+
description="A research agent",
110+
instructions="Research topics carefully.",
111+
),
112+
]
113+
create_deep_agent(model=TEST_MODEL, subagents=subagents)
114+
factory = subagents[0]["agent_factory"]
115+
assert factory is not None
116+
sub_agent = factory({"instructions": "Research topics carefully.", "model": TEST_MODEL})
117+
assert any(BASE_PROMPT in str(i) for i in sub_agent._instructions)
118+
assert any("Research topics carefully." in str(i) for i in sub_agent._instructions)
119+
120+
def test_default_subagent_factory_no_task_instructions(self):
121+
"""Subagent factory with empty instructions uses only BASE_PROMPT."""
122+
from pydantic_deep.prompts import BASE_PROMPT
123+
124+
subagents: list[SubAgentConfig] = [
125+
SubAgentConfig(
126+
name="helper",
127+
description="A helper agent",
128+
instructions="",
129+
),
130+
]
131+
create_deep_agent(model=TEST_MODEL, subagents=subagents)
132+
factory = subagents[0]["agent_factory"]
133+
assert factory is not None
134+
sub_agent = factory({"instructions": "", "model": TEST_MODEL})
135+
assert any(BASE_PROMPT in str(i) for i in sub_agent._instructions)
136+
102137
def test_create_with_interrupt_on(self):
103138
"""Test creating an agent with interrupt_on config."""
104139
agent = create_deep_agent(
@@ -133,14 +168,30 @@ def test_base_prompt_is_default(self):
133168
# pydantic-ai stores instructions as a normalized list
134169
assert any(BASE_PROMPT in str(i) for i in agent._instructions)
135170

136-
def test_custom_instructions_override_base_prompt(self):
171+
def test_custom_instructions_replace_base_prompt(self):
137172
"""Custom instructions replace BASE_PROMPT entirely."""
138173
from pydantic_deep.prompts import BASE_PROMPT
139174

140175
custom = "You are a custom agent."
141176
agent = create_deep_agent(model=TEST_MODEL, instructions=custom, cost_tracking=False)
142177
assert any(custom in str(i) for i in agent._instructions)
143-
assert not any(str(i) == BASE_PROMPT for i in agent._instructions)
178+
assert not any(BASE_PROMPT in str(i) for i in agent._instructions)
179+
180+
def test_custom_instructions_with_base_prompt_fstring(self):
181+
"""User can combine BASE_PROMPT with their own instructions via f-string."""
182+
from pydantic_deep import BASE_PROMPT
183+
184+
custom = f"{BASE_PROMPT}\n\nYou are a coding assistant."
185+
agent = create_deep_agent(model=TEST_MODEL, instructions=custom, cost_tracking=False)
186+
assert any(BASE_PROMPT in str(i) for i in agent._instructions)
187+
assert any("coding assistant" in str(i) for i in agent._instructions)
188+
189+
def test_empty_instructions_uses_base_prompt(self):
190+
"""instructions=None (default) uses BASE_PROMPT."""
191+
from pydantic_deep.prompts import BASE_PROMPT
192+
193+
agent = create_deep_agent(model=TEST_MODEL, instructions=None, cost_tracking=False)
194+
assert any(BASE_PROMPT in str(i) for i in agent._instructions)
144195

145196
def test_create_with_all_capabilities_disabled(self):
146197
"""Agent can be created with all built-in capabilities disabled (all_capabilities empty)."""

0 commit comments

Comments
 (0)