pyagentic-core 2.14.1.dev1__py3-none-any.whl → 2.15.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyagentic/__init__.py CHANGED
@@ -13,7 +13,7 @@ Quick Start:
13
13
  from pyagentic import BaseAgent, tool
14
14
 
15
15
  class MyAgent(BaseAgent):
16
- __system_message__ = "You are a helpful assistant"
16
+ __instructions__ = "You are a helpful assistant"
17
17
 
18
18
  @tool("Calculate the sum of two numbers")
19
19
  def add(self, a: int, b: int) -> str:
@@ -34,6 +34,7 @@ Main Components:
34
34
  - spec: Configuration factory for state, params, and agent links
35
35
  - ref: Dynamic state reference system for tool parameters
36
36
  - AgentExtension: Base class for creating reusable agent mixins
37
+ - PromptEngine / LocalPromptEngine: Managed sources for agent instructions
37
38
  """
38
39
 
39
40
  from pyagentic._base._spec import spec
@@ -45,6 +46,7 @@ from pyagentic._base._tool import tool
45
46
  from pyagentic._base._state import State
46
47
  from pyagentic._base._depends import Depends
47
48
  from pyagentic._base._ref import ref
49
+ from pyagentic._base._prompts import PromptEngine, LocalPromptEngine, PromptRef, PromptSource
48
50
 
49
51
  __all__ = [
50
52
  "BaseAgent",
@@ -56,4 +58,8 @@ __all__ = [
56
58
  "Depends",
57
59
  "MCPLink",
58
60
  "ref",
61
+ "PromptEngine",
62
+ "LocalPromptEngine",
63
+ "PromptRef",
64
+ "PromptSource",
59
65
  ]
@@ -24,6 +24,7 @@ from pyagentic._base._state import _StateDefinition
24
24
  from pyagentic._base._metaclasses import AgentMeta
25
25
  from pyagentic._base._exceptions import InvalidLLMSetup, InvalidToolDefinition
26
26
  from pyagentic._base._info import _SpecInfo
27
+ from pyagentic._base._prompts import PromptRef
27
28
  from pyagentic._base._agent._agent_state import _AgentState
28
29
 
29
30
  if TYPE_CHECKING:
@@ -91,7 +92,7 @@ class AgentExtension:
91
92
  return f"Logged: {message}"
92
93
 
93
94
  class MyAgent(BaseAgent, LoggingMixin):
94
- __system_message__ = "You are a helpful agent with logging"
95
+ __instructions__ = "You are a helpful agent with logging"
95
96
  ```
96
97
  """
97
98
 
@@ -127,7 +128,10 @@ class BaseAgent(metaclass=AgentMeta):
127
128
 
128
129
  Agent definition requires the use of special decorators and class attributes:
129
130
  - @tool: Declares a method as a tool callable by the LLM
130
- - __system_message__: Required class attribute defining the agent's system prompt
131
+ - __instructions__: Required class attribute defining the agent's instructions,
132
+ either as a string or a PromptRef from a PromptEngine. Rendered with state
133
+ into the system message sent to the LLM. (__system_message__ is the
134
+ deprecated spelling.)
131
135
  - __description__: Optional description used when agent is linked to another agent
132
136
  - __input_template__: Optional template for formatting user input
133
137
  - __response_format__: Optional Pydantic model for structured output
@@ -175,7 +179,8 @@ class BaseAgent(metaclass=AgentMeta):
175
179
  __dependencies__: ClassVar[dict[str, type]] # Depends[T] field -> dependency type
176
180
 
177
181
  # User-set Class Attributes (defined in subclass)
178
- __system_message__: ClassVar[str] # Required: system prompt for the agent
182
+ __instructions__: ClassVar[Union[str, PromptRef]] # Required: the agent's instructions
183
+ __system_message__: ClassVar[str] # Deprecated: old spelling of __instructions__
179
184
  __description__: ClassVar[str] # Optional: description for linked agents
180
185
  __input_template__: ClassVar[str] = None # Optional: template for user input
181
186
  __response_format__: ClassVar[Type[BaseModel]] = None # Optional: structured output format
@@ -381,7 +386,7 @@ class BaseAgent(metaclass=AgentMeta):
381
386
  Example:
382
387
  ```python
383
388
  class DatabaseAgent(BaseAgent):
384
- __system_message__ = "You query databases"
389
+ __instructions__ = "You query databases"
385
390
  connection: Optional[Connection] = None
386
391
 
387
392
  def __post_init__(self):
@@ -887,6 +892,7 @@ class BaseAgent(metaclass=AgentMeta):
887
892
  "final_output": final_ai_output,
888
893
  "state": self.state,
889
894
  "provider_info": self.provider._info,
895
+ "prompt": self.state.prompt_source,
890
896
  }
891
897
  # Include tool/agent responses if any were called
892
898
  if self.__tool_defs__:
@@ -957,7 +963,7 @@ class BaseAgent(metaclass=AgentMeta):
957
963
  Example (Custom Implementation):
958
964
  ```python
959
965
  class CoursePlannerAgent(BaseAgent):
960
- __system_message__ = "You design course curricula"
966
+ __instructions__ = "You design course curricula"
961
967
  __description__ = "Creates structured course plans"
962
968
 
963
969
  async def __call__(
@@ -21,11 +21,11 @@ class Link(Generic[T]):
21
21
  Example:
22
22
  ```python
23
23
  class ResearchAgent(BaseAgent):
24
- __system_message__ = "You research topics deeply"
24
+ __instructions__ = "You research topics deeply"
25
25
  __description__ = "Research agent for gathering detailed information"
26
26
 
27
27
  class OrchestratorAgent(BaseAgent):
28
- __system_message__ = "You coordinate research tasks"
28
+ __instructions__ = "You coordinate research tasks"
29
29
 
30
30
  # Link research agent as a tool
31
31
  researcher: Link[ResearchAgent]
@@ -1,13 +1,14 @@
1
1
  import asyncio
2
2
  import threading
3
3
  from typing import Any, Type, Self, Optional, ClassVar
4
- from pydantic import BaseModel, create_model, Field, PrivateAttr, computed_field
4
+ from pydantic import BaseModel, ConfigDict, create_model, Field, PrivateAttr
5
5
  from jinja2 import Template
6
6
  from typing import Optional, Literal, Callable
7
7
  from transitions import Machine
8
8
 
9
9
  from pyagentic._base._exceptions import InvalidStateRefNotFoundInState
10
10
  from pyagentic._base._state import _StateDefinition
11
+ from pyagentic._base._prompts import PromptRef, PromptSource, _inline_source
11
12
  from pyagentic.policies._policy import Policy
12
13
  from pyagentic.policies._events import Event, EventKind, GetEvent, SetEvent, CompileEvent
13
14
  from pyagentic.policies._list import PolicyList
@@ -30,14 +31,18 @@ class _AgentState(BaseModel):
30
31
  ("background", EventKind.APPEND): "background_append",
31
32
  }
32
33
  __policies__: ClassVar[dict[str, list[Policy]]] = {}
34
+ __agent_name__: ClassVar[str] = ""
33
35
 
34
- instructions: str
36
+ model_config = ConfigDict(arbitrary_types_allowed=True)
37
+
38
+ instructions: str | PromptRef
35
39
  input_template: Optional[str] = "{{ user_message }}"
36
40
 
37
41
  _machine: Machine = PrivateAttr(default=None)
38
42
  _messages: list[Message] = PrivateAttr(default_factory=list)
39
43
  _context: list[Message] = PrivateAttr(default_factory=list)
40
44
  _last_usage: Optional[UsageInfo] = PrivateAttr(default=None)
45
+ _prompt_source: Optional[PromptSource] = PrivateAttr(default=None)
41
46
  _instructions_template: Template = PrivateAttr(default_factory=lambda: Template(source=""))
42
47
  _input_template: Template = PrivateAttr(
43
48
  default_factory=lambda: Template(source="{{ user_message }}")
@@ -73,6 +78,18 @@ class _AgentState(BaseModel):
73
78
  getattr(self._machine, trigger)()
74
79
 
75
80
  def model_post_init(self, state):
81
+ # Instructions declared as a PromptRef resolve here, at instantiation, so
82
+ # every new agent instance (and every fork) picks up the latest prompt.
83
+ # Plain strings get an "inline" PromptSource so every run still carries
84
+ # versioned prompt metadata.
85
+ if isinstance(self.instructions, PromptRef):
86
+ self._prompt_source = self.instructions.resolve()
87
+ self.instructions = self._prompt_source.text
88
+ else:
89
+ self._prompt_source = _inline_source(
90
+ self.instructions, source=self.__agent_name__ or type(self).__name__
91
+ )
92
+
76
93
  self._instructions_template = Template(source=self.instructions)
77
94
  if self.input_template:
78
95
  self._input_template = Template(source=self.input_template)
@@ -364,7 +381,10 @@ class _AgentState(BaseModel):
364
381
  raise RuntimeError(f"Unexpected ctx_map entry for {_name!r}: {definition!r}")
365
382
 
366
383
  # now build the dataclass
367
- return create_model(f"AgentState[{name}]", __base__=cls, **pydantic_fields)
384
+ model = create_model(f"AgentState[{name}]", __base__=cls, **pydantic_fields)
385
+ # Used as the PromptSource `source` for inline (plain string) instructions
386
+ model.__agent_name__ = name
387
+ return model
368
388
 
369
389
  @property
370
390
  def phase(self) -> str:
@@ -380,6 +400,18 @@ class _AgentState(BaseModel):
380
400
  """
381
401
  return self._messages[-1]
382
402
 
403
+ @property
404
+ def prompt_source(self) -> PromptSource:
405
+ """
406
+ Returns metadata about the prompt behind the instructions: the engine it
407
+ was loaded from (source_type "local", ...), or an "inline" source named
408
+ after the agent for instructions declared as a plain string.
409
+
410
+ Returns:
411
+ PromptSource: The prompt text plus source, version, and load-time metadata.
412
+ """
413
+ return self._prompt_source
414
+
383
415
  @property
384
416
  def system_message(self) -> str:
385
417
  """
@@ -28,7 +28,7 @@ class Depends(Generic[T]):
28
28
  from pyagentic import BaseAgent, State, Depends
29
29
 
30
30
  class ResearchAgent(BaseAgent):
31
- __system_message__ = "You research topics"
31
+ __instructions__ = "You research topics"
32
32
 
33
33
  topic: State[TopicState] # client-provided per session
34
34
  db: Depends[Database] # injected by the developer, by type
@@ -15,17 +15,36 @@ class InvalidToolDefinition(Exception):
15
15
  super().__init__(message)
16
16
 
17
17
 
18
- class SystemMessageNotDeclared(Exception):
18
+ class InstructionsNotDeclared(Exception):
19
19
  """
20
- Exception raised when an Agent subclass is created without declaring __system_message__.
20
+ Exception raised when an Agent subclass is created without declaring __instructions__
21
+ (or the deprecated __system_message__).
21
22
  """
22
23
 
23
24
  def __init__(self):
24
25
  super().__init__(
25
- "System message not declared on agent. Agent must be declared with `__system_message__`" # noqa E501
26
+ "Instructions not declared on agent. Agent must be declared with `__instructions__`"
26
27
  )
27
28
 
28
29
 
30
+ # Deprecated alias, kept for backwards compatibility with the old dunder name
31
+ SystemMessageNotDeclared = InstructionsNotDeclared
32
+
33
+
34
+ class PromptNotFound(Exception):
35
+ """
36
+ Exception raised when a PromptEngine cannot find a prompt for the given key.
37
+
38
+ Args:
39
+ key (str): The prompt key that was requested
40
+ source (str): Where the engine looked (e.g. the root directory)
41
+ """
42
+
43
+ def __init__(self, key, source):
44
+ message = f"Prompt '{key}' not found in {source}"
45
+ super().__init__(message)
46
+
47
+
29
48
  class UnexpectedStateItemType(Exception):
30
49
  """
31
50
  Exception raised when a state item is initialized with a value of an unexpected type.
pyagentic/_base/_mcp.py CHANGED
@@ -27,7 +27,7 @@ class MCPLink:
27
27
  Example:
28
28
  ```python
29
29
  class MyAgent(BaseAgent):
30
- __system_message__ = "You are helpful"
30
+ __instructions__ = "You are helpful"
31
31
 
32
32
  fs: MCPLink = spec.MCPLink(
33
33
  "npx",
@@ -11,7 +11,7 @@ from pydantic import BaseModel, Field, create_model
11
11
 
12
12
  from pyagentic._base._info import _SpecInfo, ParamInfo, AgentInfo, MCPInfo
13
13
  from pyagentic._base._validation import _AgentConstructionValidator
14
- from pyagentic._base._exceptions import SystemMessageNotDeclared, UnexpectedStateItemType
14
+ from pyagentic._base._exceptions import InstructionsNotDeclared, UnexpectedStateItemType
15
15
  from pyagentic._base._agent._agent_state import _AgentState
16
16
  from pyagentic._base._tool import _ToolDefinition, tool
17
17
  from pyagentic._base._state import State, StateInfo, _StateDefinition
@@ -35,7 +35,7 @@ class Agent:
35
35
  class AgentMeta(type):
36
36
  """
37
37
  Metaclass that applies only to Agent subclasses:
38
- - Ensures __system_message__ was declared
38
+ - Ensures __instructions__ (or the deprecated __system_message__) was declared
39
39
  - Collects @tool definitions and State field attributes
40
40
  - Initializes class __tool_defs__ and __state_defs__
41
41
  - Dynamically injects an __init__ signature based on class __annotations__
@@ -608,9 +608,9 @@ class AgentMeta(type):
608
608
  else:
609
609
  compiled[name] = definition.info.get_default()
610
610
 
611
- # Create the state object with system message and state fields
611
+ # Create the state object with instructions and state fields
612
612
  self.state = self.__state_class__(
613
- instructions=self.__system_message__,
613
+ instructions=self.__instructions__,
614
614
  input_template=self.__input_template__,
615
615
  **compiled,
616
616
  )
@@ -665,7 +665,7 @@ class AgentMeta(type):
665
665
 
666
666
  Inheritance is respected in MRO order. Tools, state attributes, and linked agents
667
667
  can all be inherited from other agents or mixins.
668
- __system_message__ and __input_template__ are *not* inherited.
668
+ __instructions__ and __input_template__ are *not* inherited.
669
669
 
670
670
  Args:
671
671
  name (str): Name of the new class
@@ -677,7 +677,8 @@ class AgentMeta(type):
677
677
  type: The newly created Agent subclass
678
678
 
679
679
  Raises:
680
- SystemMessageNotDeclared: If __system_message__ is not defined in the class
680
+ InstructionsNotDeclared: If neither __instructions__ nor the deprecated
681
+ __system_message__ is defined in the class
681
682
  """
682
683
 
683
684
  # Create an inherited namespace by combining all bases in MRO order
@@ -699,9 +700,22 @@ class AgentMeta(type):
699
700
  mcs.__BaseAgent__ = cls
700
701
  return cls
701
702
  cls.__abstract_base__ = False
702
- # Verify system message is set (not inherited)
703
- if "__system_message__" not in namespace:
704
- raise SystemMessageNotDeclared()
703
+ # Verify instructions are set (not inherited); __system_message__ is
704
+ # the deprecated spelling and normalizes onto __instructions__
705
+ if "__instructions__" in namespace:
706
+ cls.__instructions__ = namespace["__instructions__"]
707
+ elif "__system_message__" in namespace:
708
+ warnings.warn(
709
+ f"`__system_message__` on {name} is deprecated; "
710
+ "declare `__instructions__` instead",
711
+ DeprecationWarning,
712
+ stacklevel=2,
713
+ )
714
+ cls.__instructions__ = namespace["__system_message__"]
715
+ else:
716
+ raise InstructionsNotDeclared()
717
+ # Keep the deprecated attribute readable for backwards compatibility
718
+ cls.__system_message__ = cls.__instructions__
705
719
 
706
720
  # Extract and attach Agent attributes:
707
721
  #
@@ -0,0 +1,243 @@
1
+ """
2
+ Prompt management for agents.
3
+
4
+ A `PromptEngine` is a source agents can pull managed instructions from (local files,
5
+ a database, a prompt-management service, ...). Engines load prompt text by key and
6
+ return it as a `PromptSource` carrying version metadata. `PromptEngine.ref(key)`
7
+ produces a deferred `PromptRef` that can be assigned to `__instructions__`; the agent
8
+ resolves it at instantiation time, so every new instance (and every fork) picks up
9
+ the latest version of the prompt.
10
+ """
11
+
12
+ import hashlib
13
+ import re
14
+ from abc import ABC, abstractmethod
15
+ from datetime import datetime, timezone
16
+ from pathlib import Path
17
+
18
+ from pydantic import BaseModel
19
+
20
+ from pyagentic._base._exceptions import PromptNotFound
21
+
22
+
23
+ class PromptSource(BaseModel):
24
+ """
25
+ A loaded prompt along with metadata about where and when it was loaded.
26
+
27
+ Attributes:
28
+ text (str): The prompt text itself.
29
+ source (str): Identifier of where the prompt came from (e.g. a file path).
30
+ source_type (str): The kind of engine that loaded it (e.g. "local").
31
+ version (str): Version identifier for the loaded text (e.g. a content hash).
32
+ loaded_at (datetime): When the prompt was loaded.
33
+ """
34
+
35
+ text: str
36
+ source: str
37
+ source_type: str
38
+ version: str
39
+ loaded_at: datetime
40
+
41
+
42
+ def _version_hash(text: str) -> str:
43
+ """Content hash used as the version identifier for prompt text."""
44
+ return hashlib.sha256(text.encode()).hexdigest()[:12]
45
+
46
+
47
+ def _inline_source(text: str, source: str) -> PromptSource:
48
+ """Build a PromptSource for instructions declared inline as a plain string."""
49
+ return PromptSource(
50
+ text=text,
51
+ source=source,
52
+ source_type="inline",
53
+ version=_version_hash(text),
54
+ loaded_at=datetime.now(timezone.utc),
55
+ )
56
+
57
+
58
+ class PromptRef:
59
+ """
60
+ A deferred pointer to a prompt in a `PromptEngine`.
61
+
62
+ Created via `PromptEngine.ref(key)` and assigned to `__instructions__`; resolved
63
+ at agent instantiation, so each new instance (and each fork) reloads the prompt.
64
+ """
65
+
66
+ def __init__(self, engine: "PromptEngine", key: str, version: str | None = None):
67
+ self.engine = engine
68
+ self.key = key
69
+ self.version = version
70
+
71
+ def resolve(self) -> PromptSource:
72
+ """
73
+ Load the referenced prompt from its engine.
74
+
75
+ Returns:
76
+ PromptSource: The loaded prompt text plus metadata.
77
+
78
+ Raises:
79
+ PromptNotFound: If the engine has no prompt for the key.
80
+ """
81
+ return self.engine.load(self.key, version=self.version)
82
+
83
+ def __repr__(self) -> str:
84
+ return f"PromptRef(engine={self.engine!r}, key={self.key!r}, version={self.version!r})"
85
+
86
+
87
+ class PromptEngine(ABC):
88
+ """
89
+ Base class for prompt engines, designed to be extended to support multiple
90
+ different source types (local files, databases, managed prompt services, ...).
91
+ """
92
+
93
+ @abstractmethod
94
+ def load(self, key: str, version: str | None = None) -> PromptSource:
95
+ """
96
+ Load a prompt by key.
97
+
98
+ Args:
99
+ key (str): Engine-specific identifier for the prompt.
100
+ version (str | None): Specific version to load; the engine's latest
101
+ when omitted.
102
+
103
+ Returns:
104
+ PromptSource: The loaded prompt text plus metadata.
105
+
106
+ Raises:
107
+ PromptNotFound: If no prompt exists for the key (or version).
108
+ """
109
+
110
+ def ref(self, key: str, version: str | None = None) -> PromptRef:
111
+ """
112
+ Create a deferred reference to a prompt, resolved at agent instantiation.
113
+
114
+ Args:
115
+ key (str): Engine-specific identifier for the prompt.
116
+ version (str | None): Pin a specific version; the engine's latest
117
+ when omitted.
118
+
119
+ Returns:
120
+ PromptRef: A reference that loads the prompt when resolved.
121
+ """
122
+ return PromptRef(self, key, version=version)
123
+
124
+
125
+ def _natural_sort_key(version: str) -> list:
126
+ """Sort key treating digit runs numerically, so e.g. v10 orders after v2."""
127
+ return [int(part) if part.isdigit() else part for part in re.split(r"(\d+)", version)]
128
+
129
+
130
+ class LocalPromptEngine(PromptEngine):
131
+ """
132
+ Prompt engine backed by the local file system.
133
+
134
+ A `pattern` defines how prompt files are laid out under the root directory
135
+ (default `.prompts/` in the current working directory). The pattern is a
136
+ template with a required `{key}` placeholder and an optional `{version}`
137
+ placeholder:
138
+
139
+ - Without `{version}` (default `"{key}.md"`): the key maps to a single file
140
+ and the prompt is versioned by content hash.
141
+ - With `{version}` (e.g. `"{key}/{version}.md"`): the directory structure
142
+ holds the versions. `load(key)` picks the latest version (natural sort,
143
+ so `v10` > `v2`); `load(key, version="v1")` pins one. The path's version
144
+ segment becomes `PromptSource.version`.
145
+
146
+ Example:
147
+ ```python
148
+ # .prompts/researcher/v1.md, .prompts/researcher/v2.md, ...
149
+ prompts = LocalPromptEngine(".prompts", pattern="{key}/{version}.md")
150
+
151
+ class ResearchAgent(BaseAgent):
152
+ __instructions__ = prompts.ref("researcher") # latest version
153
+ ```
154
+ """
155
+
156
+ def __init__(self, path: str | Path | None = None, pattern: str = "{key}.md"):
157
+ """
158
+ Args:
159
+ path (str | Path | None): Root directory holding prompt files. Defaults
160
+ to `.prompts/` under the current working directory.
161
+ pattern (str): File layout template relative to the root. Must contain
162
+ `{key}`; may contain `{version}` to derive versions from the file
163
+ structure. Defaults to `"{key}.md"`.
164
+
165
+ Raises:
166
+ ValueError: If the pattern does not contain a `{key}` placeholder.
167
+ """
168
+ if "{key}" not in pattern:
169
+ raise ValueError(
170
+ f"LocalPromptEngine pattern {pattern!r} must contain a '{{key}}' placeholder"
171
+ )
172
+ self.root = Path(path) if path else Path.cwd() / ".prompts"
173
+ self.pattern = pattern
174
+ self.versioned = "{version}" in pattern
175
+
176
+ def _resolve_versioned(self, key: str, version: str | None) -> tuple[Path, str]:
177
+ """Resolve path and version for a versioned pattern; latest version when unpinned."""
178
+ if version is not None:
179
+ path = self.root / self.pattern.format(key=key, version=version)
180
+ if not path.is_file():
181
+ raise PromptNotFound(key=f"{key} (version {version})", source=str(self.root))
182
+ return path, version
183
+
184
+ # Glob for every version of the key, then parse the version segment back
185
+ # out of each path with a regex built from the pattern
186
+ regex = re.compile(
187
+ re.escape(self.pattern)
188
+ .replace(re.escape("{key}"), re.escape(key))
189
+ .replace(re.escape("{version}"), r"(?P<version>[^/]+)")
190
+ )
191
+ found: dict[str, Path] = {}
192
+ for path in self.root.glob(self.pattern.format(key=key, version="*")):
193
+ match = regex.fullmatch(path.relative_to(self.root).as_posix())
194
+ if match:
195
+ found[match.group("version")] = path
196
+
197
+ if not found:
198
+ raise PromptNotFound(key=key, source=str(self.root))
199
+ latest = max(found, key=_natural_sort_key)
200
+ return found[latest], latest
201
+
202
+ def load(self, key: str, version: str | None = None) -> PromptSource:
203
+ """
204
+ Load a prompt from a file laid out per the engine's pattern.
205
+
206
+ Args:
207
+ key (str): Prompt identifier substituted into the pattern's `{key}`.
208
+ version (str | None): Specific version to load. Only valid for
209
+ versioned patterns; the latest version is used when omitted.
210
+
211
+ Returns:
212
+ PromptSource: The file contents. Versioned patterns take the version
213
+ from the file structure; unversioned patterns use a content hash.
214
+
215
+ Raises:
216
+ PromptNotFound: If no matching file exists under the root.
217
+ ValueError: If a version is requested but the pattern has no
218
+ `{version}` placeholder.
219
+ """
220
+ if self.versioned:
221
+ path, resolved_version = self._resolve_versioned(key, version)
222
+ else:
223
+ if version is not None:
224
+ raise ValueError(
225
+ f"Engine pattern {self.pattern!r} has no '{{version}}' placeholder; "
226
+ "a specific version cannot be requested"
227
+ )
228
+ path = self.root / self.pattern.format(key=key)
229
+ if not path.is_file():
230
+ raise PromptNotFound(key=key, source=str(self.root))
231
+ resolved_version = None
232
+
233
+ text = path.read_text()
234
+ return PromptSource(
235
+ text=text,
236
+ source=str(path),
237
+ source_type="local",
238
+ version=resolved_version or _version_hash(text),
239
+ loaded_at=datetime.now(timezone.utc),
240
+ )
241
+
242
+ def __repr__(self) -> str:
243
+ return f"LocalPromptEngine(root={str(self.root)!r}, pattern={self.pattern!r})"
pyagentic/_base/_ref.py CHANGED
@@ -79,7 +79,7 @@ class _RefRoot:
79
79
  from pyagentic import ref
80
80
 
81
81
  class Agent(BaseAgent):
82
- __system_message__ = "You are helpful"
82
+ __instructions__ = "You are helpful"
83
83
  conversation: State[Conversation]
84
84
 
85
85
  @tool("Continue the conversation")
pyagentic/_base/_spec.py CHANGED
@@ -26,7 +26,7 @@ class spec:
26
26
  email: str
27
27
 
28
28
  class MyAgent(BaseAgent):
29
- __system_message__ = "You are a helpful assistant"
29
+ __instructions__ = "You are a helpful assistant"
30
30
 
31
31
  # State with default factory
32
32
  profile: State[UserProfile] = spec.State(
pyagentic/_base/_state.py CHANGED
@@ -27,7 +27,7 @@ class State(Generic[T]):
27
27
  message_count: int = 0
28
28
 
29
29
  class ChatAgent(BaseAgent):
30
- __system_message__ = "You are a chatbot"
30
+ __instructions__ = "You are a chatbot"
31
31
 
32
32
  # Simple state field
33
33
  conversation: State[ConversationState]
pyagentic/_base/_tool.py CHANGED
@@ -256,7 +256,7 @@ def tool(description: str, condition: Callable[[Any], bool] = None, phases: list
256
256
  Example:
257
257
  ```python
258
258
  class FileAgent(BaseAgent):
259
- __system_message__ = "You help manage files"
259
+ __instructions__ = "You help manage files"
260
260
 
261
261
  @tool("Read the contents of a file given its path")
262
262
  def read_file(self, path: str) -> str:
@@ -2,6 +2,7 @@ from pydantic import BaseModel, Field, create_model
2
2
  from typing import Type, Self, Union, Any, Literal, Optional
3
3
 
4
4
  from pyagentic._base._tool import _ToolDefinition
5
+ from pyagentic._base._prompts import PromptSource
5
6
  from pyagentic._base._agent._agent_state import _AgentState
6
7
 
7
8
  from pyagentic._utils._typing import TypeCategory, analyze_type
@@ -97,6 +98,14 @@ class AgentResponse(BaseModel):
97
98
 
98
99
  final_output: Union[str, Type[BaseModel]]
99
100
  provider_info: ProviderInfo
101
+ prompt: Optional[PromptSource] = Field(
102
+ default=None,
103
+ description=(
104
+ "The prompt behind the agent's instructions: loaded from a PromptEngine "
105
+ "(e.g. source_type 'local'), or an 'inline' source named after the agent "
106
+ "for instructions declared as a plain string."
107
+ ),
108
+ )
100
109
 
101
110
  @classmethod
102
111
  def from_agent_class(
@@ -69,7 +69,7 @@ class Policy(Protocol[T]):
69
69
 
70
70
  # Use in agent
71
71
  class CounterAgent(BaseAgent):
72
- __system_message__ = "You manage a counter"
72
+ __instructions__ = "You manage a counter"
73
73
  counter: State[int] = spec.State(default=0, policies=[ValidationPolicy()])
74
74
  ```
75
75
  """
@@ -4,7 +4,7 @@ Prewritten message-context policies for managing agent context growth.
4
4
  Attach these to an agent via the `__message_policies__` class attribute:
5
5
 
6
6
  class MyAgent(BaseAgent):
7
- __system_message__ = "..."
7
+ __instructions__ = "..."
8
8
  __message_policies__ = [
9
9
  ToolOutputClipPolicy(max_chars=8000),
10
10
  ToolEvictionPolicy(keep_last_n=5),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyagentic-core
3
- Version: 2.14.1.dev1
3
+ Version: 2.15.1.dev1
4
4
  Summary: Build LLM Agents in a Pythonic way
5
5
  Author-email: Ryan Mikulec <rmikulec.dev@gmail.com>
6
6
  License: MIT
@@ -1,22 +1,23 @@
1
- pyagentic/__init__.py,sha256=S1nJnbbDZzkH0AbKYWIAWCPhZJW2a2Ml2zdxi91WSPk,2012
1
+ pyagentic/__init__.py,sha256=eHUKxFTSj5UH8ncFubnmXpS1TomGgHHrwRlFnNzl_3U,2265
2
2
  pyagentic/_version_scheme.py,sha256=hUYPvGhWz5L7RSuVxIqW3h10i4zNjfYXMofmoIk8ZBM,1569
3
3
  pyagentic/logging.py,sha256=vWLKGx0jZurWQT5pMr6fLH5NgJnZd_eaB97geBnqlwI,1718
4
4
  pyagentic/updates.py,sha256=JrBxbmTRxS-4tfSrai_wPK-dxSyCiNrYwC5MgrzGj54,870
5
5
  pyagentic/_base/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- pyagentic/_base/_depends.py,sha256=O59cvg4uc4feDrPjn9HqryrMGzqNyulyv8intAmQ8PE,2188
7
- pyagentic/_base/_exceptions.py,sha256=r_wcuWgc_Y3g_D7DwLQL8NbTphIAeeNpIp-WDHMvvOk,3746
6
+ pyagentic/_base/_depends.py,sha256=e9j6GiYvPJAmFExTHMeg-xcFGYvvfF9KtCstX5HDS7Y,2186
7
+ pyagentic/_base/_exceptions.py,sha256=Z1AvSEGJu0s8dE6F8iNGBHcNOmwlaS1gA1R15ezQYg4,4298
8
8
  pyagentic/_base/_info.py,sha256=LeXn3VbEJkP4KmMpjY6bqXjHYnE-ZHLuKoRcrmrqqhs,3020
9
- pyagentic/_base/_mcp.py,sha256=YPRaqVFrgKDA5BZtvVrcScyADjo66cdB4_h6nXpRJBI,8329
10
- pyagentic/_base/_metaclasses.py,sha256=4Ge9d4TDmQdrIGmVLKAif94Yt-wKbRB12prsxT5FgC0,36292
11
- pyagentic/_base/_ref.py,sha256=Ez1Iexxl7NLBK91frcgz_eFbrXfLZpm9fvkYHMFWA_w,3179
12
- pyagentic/_base/_spec.py,sha256=jV0iJZycJEG8fUi9qcH0NxpxoRHiB8M_YrRrmoMwSGQ,7672
13
- pyagentic/_base/_state.py,sha256=swjMl22rOgaIVEuqyL1bG7gpYpumuOZ6ytNImV_2lGw,1794
14
- pyagentic/_base/_tool.py,sha256=6Zkd20iwQX1uS-rw4i9IA8doM8qjchEZ3EmMk5Li9SE,11040
9
+ pyagentic/_base/_mcp.py,sha256=jclmFb5yB1szg9PTskPE2hLO3fvnFKJQFnnwTLdf0IY,8327
10
+ pyagentic/_base/_metaclasses.py,sha256=jvDuFoz2DAaVxu2vFkAbyT6W61euaDyKW3V0w5dS3kI,37067
11
+ pyagentic/_base/_prompts.py,sha256=aSUptdlKhUuJVrIb20FOvICzxp6pcmeb65Xf2goFpvk,9118
12
+ pyagentic/_base/_ref.py,sha256=E-1DGVoM2xub476BrHOkxTqzdw-8t_N6XuEerXDYYE8,3177
13
+ pyagentic/_base/_spec.py,sha256=Mx8tXF8ERpsuxizRXuq3wJavbXvCCGUn17YbjiOKP9Y,7670
14
+ pyagentic/_base/_state.py,sha256=-xYVIaKiaU9ERuPf-33nbUhbvI3DgIp36y1bYdYTwJU,1792
15
+ pyagentic/_base/_tool.py,sha256=03VQmY_Q_2NkNJxrHFkAzJJ09679Fb00h8ZHYrYDGUU,11038
15
16
  pyagentic/_base/_validation.py,sha256=BBscZMrXB09LNNP17pYOAY6I4iCW8r2GBVStqBd-9f8,3833
16
17
  pyagentic/_base/_agent/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- pyagentic/_base/_agent/_agent.py,sha256=DLsrCeSuD9kvL2yiIEs2GuGSR0v89sK0uWYQNPaRQF4,43268
18
- pyagentic/_base/_agent/_agent_linking.py,sha256=gSHQkbH-BeKG8moROKDZiLs0ktLzW9bOVo0DFq3UNs8,1841
19
- pyagentic/_base/_agent/_agent_state.py,sha256=NJ4WOkP7_UuQ5ycGz7oOKwMd5waD8QcpZucgkCaNOHw,17161
18
+ pyagentic/_base/_agent/_agent.py,sha256=eyPqISReeTC-yOjva5BNFTs5dr3Qtw-is_yNyt6JWo0,43659
19
+ pyagentic/_base/_agent/_agent_linking.py,sha256=2lVICn9SSk2dgDmdk_awIS7NaSLp-LXvpF_9L3Jaud8,1837
20
+ pyagentic/_base/_agent/_agent_state.py,sha256=0YkbUZT-dWHWgBUoMaPjTJ3rNxE-cX0_pIqrLa7nLWM,18628
20
21
  pyagentic/_utils/_typing.py,sha256=5aRZZOVzR0ZgnBfejkw__1yoARhn70P5QzA81sxUkls,3681
21
22
  pyagentic/_utils/_warnings.py,sha256=mjlBiYHGnnh5599Gsq5ke8mBFS6WsdhNa7jk20TT9_U,1330
22
23
  pyagentic/api/__init__.py,sha256=7CklNYjxVj5XOyqTWvvWKZU6IYuU8CAfpOhNeGMH1d4,1712
@@ -44,21 +45,21 @@ pyagentic/llm/_mock.py,sha256=X_Rj7DP_RV_hKJth6exjl90bJGHC3xTy8-7da6lOg3k,3282
44
45
  pyagentic/llm/_openai.py,sha256=y1rY35O2X78fbDgj8XNVRs6JDEvtBQ2w3NdXyb9_BbA,5994
45
46
  pyagentic/llm/_provider.py,sha256=McxbFcRD7UTtpnmGwYieGGBrHb6JfzZMkfnFxhj6Vrw,2508
46
47
  pyagentic/models/llm.py,sha256=hx8x4d2SJGxlqGSiN7Ewdopvo87IUyJELoR3wR8KV0s,5126
47
- pyagentic/models/response.py,sha256=Zs4_yLKyZue3ysKrGQcjECmPAXJi_NJ_bEP8tINBVJo,5894
48
+ pyagentic/models/response.py,sha256=nnSEzuH-DpFTWhV5mC5WZstX_Gm44YxZ96V3Zq7qeQU,6281
48
49
  pyagentic/models/tracing.py,sha256=5_cGxOAb5I9qjLVwSdvIcxUzrZy56OUe5fxlxOkud_w,1218
49
50
  pyagentic/policies/__init__.py,sha256=WCgpEA1KZ23iJW5bkT32sfP8jLSLso21xJY5ZUKJVTQ,451
50
51
  pyagentic/policies/_events.py,sha256=hTZH4VT9n2Ba4ICNiec32CMPGptyO02tjsui01XEqUY,2586
51
52
  pyagentic/policies/_list.py,sha256=Whz4T5lxodzKqZNrr7opolJPvfMhYfM6gn-0GTwuqeQ,5865
52
- pyagentic/policies/_policy.py,sha256=WvPO-vWWt3MyQK3TG1AQrZx-TCGPNLMv4mRVjd95Ems,3907
53
- pyagentic/policies/messages.py,sha256=45RYHmOC6ihF0YmS10MNOaYmlicfZ9Ni1hQx-FuxKSc,8192
53
+ pyagentic/policies/_policy.py,sha256=xsvm3NBraTBcNr8RcHRAasmvc6ttiAF1qCKFvG-kkRQ,3905
54
+ pyagentic/policies/messages.py,sha256=RVIr9GwUXjw3We1fZaXPxjByqZIvu4uNYcFti5YT6V4,8190
54
55
  pyagentic/tracing/__init__.py,sha256=ddMOl_-Nf3WOhu-PYxSJ3kqHZ3eQXAOM5aySn9YmDtA,150
55
56
  pyagentic/tracing/_basic.py,sha256=jUBKdkOPveCjBzEhHKS_Rvp5l38iZg-5nFIl6xEwOVk,8906
56
57
  pyagentic/tracing/_langfuse.py,sha256=5b5tdT-gyRJP5HDcvCS4tn_k3AFDI6eNsXOd6On5gKQ,13986
57
58
  pyagentic/tracing/_tracer.py,sha256=0uQeqJxPRjfQtvgglF2BPDQM_1fpLBxWB0PpU6KY1eA,8157
58
- pyagentic_core-2.14.1.dev1.dist-info/licenses/LICENSE,sha256=wcOzTj82hOc96HztJk2VzLB2hFHpdIGpjz8vvbpP1_s,1069
59
- pyagentic_core-2.14.1.dev1.dist-info/METADATA,sha256=k5LET3i9sSG9rDltFkrXpg-o8FsThvRZ1MHsZEzqYoc,8212
60
- pyagentic_core-2.14.1.dev1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
61
- pyagentic_core-2.14.1.dev1.dist-info/scm_file_list.json,sha256=4jMpoLHxqkHhsscGA4biTgu0XoDt9d60_L6EAX-ebJQ,4790
62
- pyagentic_core-2.14.1.dev1.dist-info/scm_version.json,sha256=LgdOXfNRMrVAcA2OqftqGPpJxBZLgZJj52D5CpLNopk,162
63
- pyagentic_core-2.14.1.dev1.dist-info/top_level.txt,sha256=lAWfd-ay434uEpSg2FM0ySN0o4vgNGE6D9dJkIhGyfY,10
64
- pyagentic_core-2.14.1.dev1.dist-info/RECORD,,
59
+ pyagentic_core-2.15.1.dev1.dist-info/licenses/LICENSE,sha256=wcOzTj82hOc96HztJk2VzLB2hFHpdIGpjz8vvbpP1_s,1069
60
+ pyagentic_core-2.15.1.dev1.dist-info/METADATA,sha256=KY3iq_69tHikWHrDebuiW-f_br1APh1m4P80R_Rdhws,8212
61
+ pyagentic_core-2.15.1.dev1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
62
+ pyagentic_core-2.15.1.dev1.dist-info/scm_file_list.json,sha256=8UMtz5WpdhOSRNM1zB-dxNzobbueYJYYqfBJoM4n6_w,4883
63
+ pyagentic_core-2.15.1.dev1.dist-info/scm_version.json,sha256=ALVMEOHx9YqLEgimG8GUehjgTfSpP-8g_W6SqxQYmSE,162
64
+ pyagentic_core-2.15.1.dev1.dist-info/top_level.txt,sha256=lAWfd-ay434uEpSg2FM0ySN0o4vgNGE6D9dJkIhGyfY,10
65
+ pyagentic_core-2.15.1.dev1.dist-info/RECORD,,
@@ -16,6 +16,7 @@
16
16
  "docs/tools.md",
17
17
  "docs/execution-modes.md",
18
18
  "docs/structured-output.md",
19
+ "docs/prompts.md",
19
20
  "docs/agent-linking.md",
20
21
  "docs/index.md",
21
22
  "docs/inheritance.md",
@@ -75,6 +76,7 @@
75
76
  "tests/_base/test_policy_list.py",
76
77
  "tests/_base/test_params.py",
77
78
  "tests/_base/test_agent_forking.py",
79
+ "tests/_base/test_prompts.py",
78
80
  "tests/_base/test_state.py",
79
81
  "tests/_base/test_agent_provider.py",
80
82
  "pyagentic/__init__.py",
@@ -128,6 +130,7 @@
128
130
  "pyagentic/_base/_state.py",
129
131
  "pyagentic/_base/_exceptions.py",
130
132
  "pyagentic/_base/_ref.py",
133
+ "pyagentic/_base/_prompts.py",
131
134
  "pyagentic/_base/_validation.py",
132
135
  "pyagentic/_base/_depends.py",
133
136
  "pyagentic/_base/_agent/__init__.py",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.15.0a1",
3
+ "distance": 1,
4
+ "node": "g26af3c3b63e03e136fae8fa6570e033dfc025e82",
5
+ "dirty": true,
6
+ "branch": "main",
7
+ "node_date": "2026-07-09"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.14.0a1",
3
- "distance": 1,
4
- "node": "g2629fed5f4a99c79b17abf20d6df0918a0a8bf83",
5
- "dirty": true,
6
- "branch": "main",
7
- "node_date": "2026-07-06"
8
- }