agentframe-py 0.1.0__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.
Files changed (46) hide show
  1. agentframe/__init__.py +170 -0
  2. agentframe/authoring/__init__.py +1 -0
  3. agentframe/authoring/agent.py +75 -0
  4. agentframe/authoring/context.py +123 -0
  5. agentframe/authoring/decorators.py +202 -0
  6. agentframe/authoring/definitions.py +372 -0
  7. agentframe/authoring/services.py +176 -0
  8. agentframe/cli.py +293 -0
  9. agentframe/errors.py +194 -0
  10. agentframe/hosting/__init__.py +1 -0
  11. agentframe/hosting/a2a_executor.py +569 -0
  12. agentframe/hosting/admission.py +69 -0
  13. agentframe/hosting/app.py +617 -0
  14. agentframe/hosting/card.py +138 -0
  15. agentframe/hosting/dedup.py +193 -0
  16. agentframe/hosting/draining_server.py +47 -0
  17. agentframe/hosting/host.py +143 -0
  18. agentframe/hosting/lifecycle.py +81 -0
  19. agentframe/hosting/model_router.py +407 -0
  20. agentframe/hosting/request_size.py +52 -0
  21. agentframe/hosting/routing.py +349 -0
  22. agentframe/models/__init__.py +1 -0
  23. agentframe/models/base.py +264 -0
  24. agentframe/models/foundry.py +570 -0
  25. agentframe/models/ollama.py +320 -0
  26. agentframe/models/strict_schema.py +211 -0
  27. agentframe/py.typed +0 -0
  28. agentframe/runtime/__init__.py +1 -0
  29. agentframe/runtime/generation.py +725 -0
  30. agentframe/runtime/limits.py +124 -0
  31. agentframe/runtime/options.py +50 -0
  32. agentframe/runtime/runtime.py +449 -0
  33. agentframe/runtime/tools.py +216 -0
  34. agentframe/runtime/usage.py +40 -0
  35. agentframe/security/__init__.py +1 -0
  36. agentframe/security/authentication.py +265 -0
  37. agentframe/security/authorization.py +66 -0
  38. agentframe/security/entra.py +260 -0
  39. agentframe/security/execution_policy.py +241 -0
  40. agentframe/security/principal.py +16 -0
  41. agentframe/testing.py +135 -0
  42. agentframe_py-0.1.0.dist-info/METADATA +570 -0
  43. agentframe_py-0.1.0.dist-info/RECORD +46 -0
  44. agentframe_py-0.1.0.dist-info/WHEEL +4 -0
  45. agentframe_py-0.1.0.dist-info/entry_points.txt +2 -0
  46. agentframe_py-0.1.0.dist-info/licenses/LICENSE +201 -0
agentframe/__init__.py ADDED
@@ -0,0 +1,170 @@
1
+ """AgentFrame: define agents with Python, communicate through A2A."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from agentframe.authoring.agent import Agent
6
+ from agentframe.authoring.context import RunContext
7
+ from agentframe.authoring.decorators import agent, skill, tool
8
+ from agentframe.authoring.definitions import SkillDefinition, ToolDefinition
9
+ from agentframe.authoring.services import Inject, Services
10
+ from agentframe.errors import (
11
+ AgentFrameError,
12
+ AuthenticationError,
13
+ CancelledError,
14
+ DefinitionError,
15
+ IncompleteOutputError,
16
+ InputValidationError,
17
+ LimitExceededError,
18
+ ModelCallLimitExceededError,
19
+ ModelError,
20
+ ModelRefusalError,
21
+ OutputValidationError,
22
+ PermissionDeniedError,
23
+ PolicyUnavailableError,
24
+ ProviderError,
25
+ TimeoutLimitExceededError,
26
+ TokenLimitExceededError,
27
+ ToolCallLimitExceededError,
28
+ UnsupportedCapabilityError,
29
+ )
30
+ from agentframe.hosting.app import AgentApp
31
+ from agentframe.hosting.dedup import MessageDedupStore
32
+ from agentframe.hosting.host import A2AConfig, HostConfig
33
+ from agentframe.hosting.model_router import ModelRouter
34
+ from agentframe.hosting.routing import (
35
+ Clarification,
36
+ RoutingContext,
37
+ SkillCall,
38
+ SkillRouter,
39
+ )
40
+ from agentframe.models.base import (
41
+ Message,
42
+ Model,
43
+ ModelCapabilities,
44
+ ModelRequest,
45
+ ModelResponse,
46
+ ModelSettings,
47
+ OutputSchema,
48
+ Role,
49
+ ToolCall,
50
+ ToolSpec,
51
+ )
52
+ from agentframe.runtime.limits import RunLimits
53
+ from agentframe.runtime.options import RunOptions
54
+ from agentframe.runtime.runtime import RunResult, Runtime
55
+ from agentframe.runtime.usage import FinishReason, Usage
56
+ from agentframe.security.authentication import (
57
+ AllowAnonymous,
58
+ APIKey,
59
+ APIKeyAuth,
60
+ Authenticator,
61
+ DeclaresCardSecurity,
62
+ VerifiedCaller,
63
+ )
64
+ from agentframe.security.authorization import (
65
+ AuthorizationPolicy,
66
+ ToolAuthorizationPolicy,
67
+ )
68
+ from agentframe.security.execution_policy import (
69
+ ExecutionPolicy,
70
+ InputCheck,
71
+ ModelRequestCheck,
72
+ OutputCheck,
73
+ ToolCheck,
74
+ )
75
+ from agentframe.security.principal import Principal
76
+
77
+ if TYPE_CHECKING:
78
+ from agentframe.models.foundry import FoundryModel as FoundryModel
79
+ from agentframe.models.ollama import OllamaModel as OllamaModel
80
+ from agentframe.security.entra import EntraIDAuth as EntraIDAuth
81
+
82
+ __version__ = "0.1.0"
83
+
84
+ __all__ = [
85
+ "A2AConfig",
86
+ "APIKey",
87
+ "APIKeyAuth",
88
+ "Agent",
89
+ "AgentApp",
90
+ "AgentFrameError",
91
+ "AllowAnonymous",
92
+ "AuthenticationError",
93
+ "Authenticator",
94
+ "AuthorizationPolicy",
95
+ "CancelledError",
96
+ "Clarification",
97
+ "DeclaresCardSecurity",
98
+ "DefinitionError",
99
+ "ExecutionPolicy",
100
+ "FinishReason",
101
+ "HostConfig",
102
+ "IncompleteOutputError",
103
+ "Inject",
104
+ "InputCheck",
105
+ "InputValidationError",
106
+ "LimitExceededError",
107
+ "Message",
108
+ "MessageDedupStore",
109
+ "Model",
110
+ "ModelCallLimitExceededError",
111
+ "ModelCapabilities",
112
+ "ModelError",
113
+ "ModelRefusalError",
114
+ "ModelRequest",
115
+ "ModelRequestCheck",
116
+ "ModelResponse",
117
+ "ModelRouter",
118
+ "ModelSettings",
119
+ "OutputCheck",
120
+ "OutputSchema",
121
+ "OutputValidationError",
122
+ "PermissionDeniedError",
123
+ "PolicyUnavailableError",
124
+ "Principal",
125
+ "ProviderError",
126
+ "Role",
127
+ "RoutingContext",
128
+ "RunContext",
129
+ "RunLimits",
130
+ "RunOptions",
131
+ "RunResult",
132
+ "Runtime",
133
+ "Services",
134
+ "SkillCall",
135
+ "SkillDefinition",
136
+ "SkillRouter",
137
+ "TimeoutLimitExceededError",
138
+ "TokenLimitExceededError",
139
+ "ToolAuthorizationPolicy",
140
+ "ToolCall",
141
+ "ToolCallLimitExceededError",
142
+ "ToolCheck",
143
+ "ToolDefinition",
144
+ "ToolSpec",
145
+ "UnsupportedCapabilityError",
146
+ "Usage",
147
+ "VerifiedCaller",
148
+ "__version__",
149
+ "agent",
150
+ "skill",
151
+ "tool",
152
+ ]
153
+
154
+
155
+ def __getattr__(name: str) -> object:
156
+ # Provider adapters and EntraIDAuth are imported on first use so that
157
+ # ``import agentframe`` works without their optional extras installed.
158
+ if name == "FoundryModel":
159
+ from agentframe.models.foundry import FoundryModel as FoundryModel
160
+
161
+ return FoundryModel
162
+ if name == "OllamaModel":
163
+ from agentframe.models.ollama import OllamaModel as OllamaModel
164
+
165
+ return OllamaModel
166
+ if name == "EntraIDAuth":
167
+ from agentframe.security.entra import EntraIDAuth as EntraIDAuth
168
+
169
+ return EntraIDAuth
170
+ raise AttributeError(f"module 'agentframe' has no attribute {name!r}")
@@ -0,0 +1 @@
1
+ """Authoring: the Agent base class, decorators, definitions, context and services."""
@@ -0,0 +1,75 @@
1
+ """The Agent base class."""
2
+
3
+ from collections.abc import Callable, Iterable, Mapping
4
+ from types import MappingProxyType
5
+ from typing import Any, ClassVar
6
+
7
+ from agentframe.authoring.definitions import (
8
+ AgentDefinition,
9
+ ToolDefinition,
10
+ tools_by_name,
11
+ )
12
+ from agentframe.errors import DefinitionError
13
+ from agentframe.models.base import Model, ModelSettings
14
+ from agentframe.runtime.limits import RunLimits
15
+
16
+ DEFINITION_ATTRIBUTE = "__agentframe_definition__"
17
+
18
+
19
+ class Agent:
20
+ """Base class for agents: declare skills with @skill, describe it with @agent.
21
+
22
+ Args:
23
+ model: The model ``ctx.generate`` calls. Model-free agents omit it.
24
+ The application owns the model and closes it.
25
+ tools: ``@tool`` functions this agent's ``ctx.generate`` calls may
26
+ select. Registering grants nothing by itself: each call names the
27
+ tools it offers the model. Tools are never advertised as skills.
28
+
29
+ Raises:
30
+ DefinitionError: A tool is not decorated with ``@tool``, or two tools
31
+ share a name.
32
+
33
+ Attributes:
34
+ limits: Default execution limits for this agent's skills. Fields it
35
+ sets override the library defaults; fields a skill or run sets
36
+ override it in turn.
37
+ instructions: Trusted instructions sent as the first ``system`` message
38
+ of every ``ctx.generate`` call. Never put untrusted content here.
39
+ model_settings: Default settings for this agent's model calls, merged
40
+ field by field like ``limits``.
41
+ model: The model passed to the constructor, or ``None``.
42
+ tools: The registered tools by name, read-only.
43
+ """
44
+
45
+ limits: ClassVar[RunLimits] = RunLimits()
46
+ instructions: ClassVar[str | None] = None
47
+ model_settings: ClassVar[ModelSettings] = ModelSettings()
48
+ # Class defaults keep subclasses that do not call super().__init__() working.
49
+ model: Model | None = None
50
+ tools: Mapping[str, ToolDefinition] = MappingProxyType({})
51
+
52
+ def __init__(
53
+ self,
54
+ *,
55
+ model: Model | None = None,
56
+ tools: Iterable[Callable[..., Any]] = (),
57
+ ) -> None:
58
+ self.model = model
59
+ self.tools = tools_by_name(tools)
60
+
61
+ @classmethod
62
+ def definition(cls) -> AgentDefinition:
63
+ """Return the validated definition created by ``@agent``.
64
+
65
+ Raises:
66
+ DefinitionError: If this exact class was not decorated with ``@agent``.
67
+ """
68
+ # Look only at this class so an undecorated subclass cannot silently
69
+ # reuse its parent's definition and skills.
70
+ definition = cls.__dict__.get(DEFINITION_ATTRIBUTE)
71
+ if not isinstance(definition, AgentDefinition):
72
+ raise DefinitionError(
73
+ f"Agent '{cls.__name__}' is not decorated with @agent"
74
+ )
75
+ return definition
@@ -0,0 +1,123 @@
1
+ """Per-run context passed to skills."""
2
+
3
+ from collections.abc import Callable, Iterable
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, overload
6
+
7
+ from pydantic import BaseModel
8
+
9
+ from agentframe.runtime.generation import GenerationState
10
+ from agentframe.security.principal import Principal
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class RunContext:
15
+ """Per-run information passed to every skill. Never shared between runs.
16
+
17
+ Attributes:
18
+ invocation_id: Library-generated identifier of this run.
19
+ principal: Verified caller identity, or ``None`` when none was supplied.
20
+ deadline: Event-loop time (``loop.time()``) when the run times out.
21
+ """
22
+
23
+ invocation_id: str
24
+ principal: Principal | None
25
+ deadline: float
26
+ # Private per-run model state; the context is frozen but its accounting
27
+ # must change as the run makes model calls.
28
+ _generation: GenerationState = field(repr=False, compare=False)
29
+
30
+ @overload
31
+ async def generate(
32
+ self,
33
+ text: str,
34
+ *,
35
+ instructions: str | None = None,
36
+ tools: Iterable[Callable[..., Any]] = (),
37
+ ) -> str: ...
38
+
39
+ @overload
40
+ async def generate[T: BaseModel](
41
+ self,
42
+ text: str,
43
+ *,
44
+ instructions: str | None = None,
45
+ output: type[T],
46
+ tools: Iterable[Callable[..., Any]] = (),
47
+ ) -> T: ...
48
+
49
+ async def generate(
50
+ self,
51
+ text: str,
52
+ *,
53
+ instructions: str | None = None,
54
+ output: type[BaseModel] | None = None,
55
+ tools: Iterable[Callable[..., Any]] = (),
56
+ ) -> str | BaseModel:
57
+ """Generate text, or a validated ``output`` instance, with the agent's model.
58
+
59
+ The agent's ``instructions`` and then ``instructions`` are sent as
60
+ trusted ``system`` messages; ``text`` is sent as a separate ``user``
61
+ message and never merged into them. Keeping them separate does not
62
+ guarantee resistance to prompt injection.
63
+
64
+ Args:
65
+ text: Untrusted content for the model, such as caller input.
66
+ instructions: Trusted instructions for this call only. Never put
67
+ untrusted content here.
68
+ output: A Pydantic ``BaseModel`` subclass. When given, the model
69
+ is asked for JSON matching its schema and the reply is
70
+ validated into an instance. Invalid output is never repaired
71
+ or re-requested.
72
+ tools: ``@tool`` functions to offer the model, each registered on
73
+ the agent and selected once. Omitted or empty offers no
74
+ tools; registered tools are never offered unless selected
75
+ here. The model's tool calls run one at a time, in its
76
+ order, and their results are sent back until it answers.
77
+ A call naming a tool that was not offered, or with invalid
78
+ arguments, runs nothing; the model is told what was wrong.
79
+ Tools whose ``requires_permissions`` the caller lacks are
80
+ not offered, and every call is authorized before it runs.
81
+
82
+ Returns:
83
+ The model's complete answer, or an ``output`` instance when
84
+ ``output`` is given. With tools, the answer after the tool calls.
85
+
86
+ Raises:
87
+ DefinitionError: ``output`` is not a ``BaseModel`` subclass, or is
88
+ a ``RootModel``, or a selected tool is not registered on the
89
+ agent or is selected twice. Raised before anything is sent.
90
+ UnsupportedCapabilityError: The agent has no model configured, or
91
+ the model lacks something the request needs, such as
92
+ structured output or tool calling. Raised before anything is
93
+ sent.
94
+ OutputValidationError: The structured reply is not valid JSON or
95
+ does not match ``output``, or a tool returned a value that
96
+ does not match its return annotation.
97
+ ModelRefusalError: The model refused, or a content filter blocked
98
+ its output.
99
+ IncompleteOutputError: The output was cut short at the token
100
+ limit or was empty. Partial text or objects are never returned;
101
+ for text cut short at the limit, the error's ``partial_text``
102
+ holds what arrived, and a skill may catch it and use it.
103
+ ProviderError: The model provider call failed.
104
+ PermissionDeniedError: The model called a tool the caller lacks
105
+ a required permission for, or the runtime's tool
106
+ authorization policy denied a call. The model is never told.
107
+ Also raised, before anything is sent, when an execution
108
+ policy model-request check blocks a request.
109
+ PolicyUnavailableError: The tool authorization policy or an
110
+ execution policy model-request check raised an error or did
111
+ not answer before the run's deadline.
112
+ LimitExceededError: A model call does not fit the run's limits,
113
+ the run has made ``max_tool_calls`` tool calls, or the run's
114
+ timeout expired during a model or tool call.
115
+
116
+ Exceptions raised by a tool propagate unchanged and fail the run;
117
+ their text is never sent to the model.
118
+ """
119
+ if output is None:
120
+ return await self._generation.generate_text(self, text, instructions, tools)
121
+ return await self._generation.generate_output(
122
+ self, text, instructions, output, tools
123
+ )
@@ -0,0 +1,202 @@
1
+ """The ``@agent``, ``@skill`` and ``@tool`` decorators."""
2
+
3
+ from collections.abc import Callable, Iterable
4
+ from typing import Any
5
+
6
+ from agentframe.authoring.agent import DEFINITION_ATTRIBUTE, Agent
7
+ from agentframe.authoring.definitions import (
8
+ TOOL_ATTRIBUTE,
9
+ AgentDefinition,
10
+ SkillDefinition,
11
+ SkillMetadata,
12
+ build_skill_definition,
13
+ build_tool_definition,
14
+ class_docstring,
15
+ )
16
+ from agentframe.errors import DefinitionError
17
+ from agentframe.models.base import ModelSettings
18
+ from agentframe.runtime.limits import RunLimits
19
+
20
+ _SKILL_ATTRIBUTE = "_skill_metadata"
21
+
22
+
23
+ # noinspection PyShadowingBuiltins
24
+ def skill[F: Callable[..., Any]](
25
+ id: str,
26
+ *,
27
+ name: str | None = None,
28
+ description: str | None = None,
29
+ tags: Iterable[str] = (),
30
+ requires_permissions: Iterable[str] = (),
31
+ limits: RunLimits | None = None,
32
+ model_settings: ModelSettings | None = None,
33
+ ) -> Callable[[F], F]:
34
+ """Mark an async ``Agent`` method as an advertised skill.
35
+
36
+ The method is inspected later by ``@agent``, once its class exists.
37
+
38
+ Args:
39
+ id: Stable skill identifier that callers select.
40
+ name: Display name. Defaults to the title-cased method name.
41
+ description: What the skill does. Defaults to the method docstring.
42
+ tags: Descriptive discovery tags. At least one non-blank tag is
43
+ required (A2A 1.0); ``@agent`` rejects a skill without one.
44
+ requires_permissions: Permissions the caller must all hold.
45
+ limits: Execution limits for this skill. Each field it sets overrides
46
+ the agent's value; unset fields are inherited.
47
+ model_settings: Model settings for this skill. Each field it sets
48
+ overrides the agent's value; unset fields are inherited.
49
+
50
+ Returns:
51
+ A decorator that records the metadata and returns the same function.
52
+
53
+ Raises:
54
+ DefinitionError: If ``id`` is empty, or ``tags`` or
55
+ ``requires_permissions`` is a single string.
56
+ """
57
+ if not id:
58
+ raise DefinitionError("Skill id must not be empty")
59
+ if isinstance(tags, str) or isinstance(requires_permissions, str):
60
+ raise DefinitionError(
61
+ f"Skill '{id}': tags and requires_permissions must be lists of strings"
62
+ )
63
+ metadata = SkillMetadata(
64
+ id=id,
65
+ name=name,
66
+ description=description,
67
+ tags=tuple(tags),
68
+ requires_permissions=frozenset(requires_permissions),
69
+ limits=limits,
70
+ model_settings=model_settings,
71
+ )
72
+
73
+ def decorator(func: F) -> F:
74
+ setattr(func, _SKILL_ATTRIBUTE, metadata)
75
+ return func
76
+
77
+ return decorator
78
+
79
+
80
+ def agent[A: type[Agent]](
81
+ version: str, *, name: str | None = None, description: str | None = None
82
+ ) -> Callable[[A], A]:
83
+ """Describe an ``Agent`` subclass and validate its skills.
84
+
85
+ Args:
86
+ version: Agent version string, for example ``"0.1.0"``.
87
+ name: Agent name. Defaults to the class name.
88
+ description: What the agent does. Defaults to the class docstring.
89
+
90
+ Returns:
91
+ A decorator that stores the definition and returns the same class.
92
+
93
+ Raises:
94
+ DefinitionError: If the class or any of its skills is invalid.
95
+ """
96
+
97
+ def decorator(cls: A) -> A:
98
+ if not (isinstance(cls, type) and issubclass(cls, Agent)):
99
+ raise DefinitionError("@agent can only decorate Agent subclasses")
100
+ agent_name = name or cls.__name__
101
+ agent_description = description or class_docstring(cls)
102
+ if not agent_description:
103
+ raise DefinitionError(
104
+ f"Agent '{agent_name}' needs a description or docstring"
105
+ )
106
+ definition = AgentDefinition(
107
+ name=agent_name,
108
+ description=agent_description,
109
+ version=version,
110
+ skills=_collect_skills(agent_name, cls),
111
+ )
112
+ setattr(cls, DEFINITION_ATTRIBUTE, definition)
113
+ return cls
114
+
115
+ return decorator
116
+
117
+
118
+ def _collect_skills(agent_name: str, cls: type) -> dict[str, SkillDefinition]:
119
+ skill_methods = _skill_methods(agent_name, cls)
120
+ if not skill_methods:
121
+ raise DefinitionError(f"Agent '{agent_name}' has no skills")
122
+
123
+ skills: dict[str, SkillDefinition] = {}
124
+ for method in skill_methods.values():
125
+ metadata: SkillMetadata = getattr(method, _SKILL_ATTRIBUTE)
126
+ if metadata.id in skills:
127
+ raise DefinitionError(
128
+ f"Agent '{agent_name}': skill id '{metadata.id}' is used by both "
129
+ f"'{skills[metadata.id].method_name}' and '{method.__name__}'"
130
+ )
131
+ skills[metadata.id] = build_skill_definition(agent_name, method, metadata)
132
+ return skills
133
+
134
+
135
+ def _skill_methods(agent_name: str, cls: type) -> dict[str, Callable[..., Any]]:
136
+ """Return skill methods by name, walking from the base class to ``cls``."""
137
+ methods: dict[str, Callable[..., Any]] = {}
138
+ for klass in reversed(cls.__mro__):
139
+ for attribute_name, value in vars(klass).items():
140
+ if hasattr(value, _SKILL_ATTRIBUTE):
141
+ methods[attribute_name] = value
142
+ elif attribute_name in methods:
143
+ raise DefinitionError(
144
+ f"Agent '{agent_name}': '{klass.__name__}.{attribute_name}' "
145
+ "overrides a skill without @skill"
146
+ )
147
+ return methods
148
+
149
+
150
+ def tool[F: Callable[..., Any]](
151
+ *,
152
+ description: str | None = None,
153
+ side_effects: bool = False,
154
+ idempotent: bool = False,
155
+ requires_permissions: Iterable[str] = (),
156
+ ) -> Callable[[F], F]:
157
+ """Mark a typed function as a tool a model may call.
158
+
159
+ Always call it with parentheses: ``@tool()`` or ``@tool(description=...)``.
160
+ The function is validated immediately and stays directly callable. Sync
161
+ functions run inline on the event loop, so use ``async def`` for any I/O.
162
+ Decorating grants nothing: the tool must still be registered on an agent
163
+ (``Agent(tools=[...])``) and selected by ``ctx.generate(..., tools=[...])``.
164
+
165
+ Args:
166
+ description: What the tool does, shown to the model. Defaults to the
167
+ function docstring; one of them is required.
168
+ side_effects: Whether a call has consequential effects, such as
169
+ cancelling an order.
170
+ idempotent: Whether repeating a call with the same arguments has no
171
+ further effect. Not a retry grant: tools are never auto-retried.
172
+ requires_permissions: Permissions the caller must all hold. A tool
173
+ the caller lacks any of is not offered to the model and fails the
174
+ run if the model calls it anyway.
175
+
176
+ Returns:
177
+ A decorator that attaches a ``ToolDefinition`` and returns the same
178
+ function (or ``staticmethod``).
179
+
180
+ Raises:
181
+ DefinitionError: If the function breaks a tool rule: it is a lambda or
182
+ method, is a generator, has an invalid name, unannotated,
183
+ ``*args``/``**kwargs`` or positional-only parameters, an
184
+ unsupported type, or no description; or if
185
+ ``requires_permissions`` is a single string.
186
+ """
187
+ if isinstance(requires_permissions, str):
188
+ raise DefinitionError("Tool requires_permissions must be a list of strings")
189
+ permissions = frozenset(requires_permissions)
190
+
191
+ def decorator(func: F) -> F:
192
+ definition = build_tool_definition(
193
+ func,
194
+ description=description,
195
+ side_effects=side_effects,
196
+ idempotent=idempotent,
197
+ requires_permissions=permissions,
198
+ )
199
+ setattr(definition.function, TOOL_ATTRIBUTE, definition)
200
+ return func
201
+
202
+ return decorator