akgentic-tool 1.2.2__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. akgentic/__init__.py +10 -0
  2. akgentic/tool/__init__.py +91 -0
  3. akgentic/tool/core.py +377 -0
  4. akgentic/tool/errors.py +22 -0
  5. akgentic/tool/event.py +149 -0
  6. akgentic/tool/knowledge_graph/__init__.py +94 -0
  7. akgentic/tool/knowledge_graph/kg_actor.py +1114 -0
  8. akgentic/tool/knowledge_graph/kg_tool.py +378 -0
  9. akgentic/tool/knowledge_graph/models.py +379 -0
  10. akgentic/tool/mcp/__init__.py +31 -0
  11. akgentic/tool/mcp/mcp.py +269 -0
  12. akgentic/tool/mcp/oauth_handler.py +250 -0
  13. akgentic/tool/planning/README.md +32 -0
  14. akgentic/tool/planning/__init__.py +17 -0
  15. akgentic/tool/planning/planning.py +334 -0
  16. akgentic/tool/planning/planning_actor.py +425 -0
  17. akgentic/tool/py.typed +2 -0
  18. akgentic/tool/sandbox/__init__.py +33 -0
  19. akgentic/tool/sandbox/actor.py +231 -0
  20. akgentic/tool/sandbox/bwrap.py +114 -0
  21. akgentic/tool/sandbox/docker.py +113 -0
  22. akgentic/tool/sandbox/local.py +148 -0
  23. akgentic/tool/sandbox/sandbox.Dockerfile +24 -0
  24. akgentic/tool/sandbox/seatbelt.py +172 -0
  25. akgentic/tool/sandbox/tool.py +213 -0
  26. akgentic/tool/search/__init__.py +10 -0
  27. akgentic/tool/search/search.py +250 -0
  28. akgentic/tool/team/__init__.py +17 -0
  29. akgentic/tool/team/team.py +544 -0
  30. akgentic/tool/vector.py +267 -0
  31. akgentic/tool/vector_store/__init__.py +57 -0
  32. akgentic/tool/vector_store/actor.py +552 -0
  33. akgentic/tool/vector_store/embedding_actor.py +192 -0
  34. akgentic/tool/vector_store/inmemory.py +286 -0
  35. akgentic/tool/vector_store/protocol.py +202 -0
  36. akgentic/tool/vector_store/tool.py +113 -0
  37. akgentic/tool/vector_store/weaviate.py +287 -0
  38. akgentic/tool/workspace/__init__.py +73 -0
  39. akgentic/tool/workspace/edit.py +373 -0
  40. akgentic/tool/workspace/readers.py +182 -0
  41. akgentic/tool/workspace/tool.py +1220 -0
  42. akgentic/tool/workspace/workspace.py +160 -0
  43. akgentic_tool-1.2.2.dist-info/METADATA +650 -0
  44. akgentic_tool-1.2.2.dist-info/RECORD +46 -0
  45. akgentic_tool-1.2.2.dist-info/WHEEL +4 -0
  46. akgentic_tool-1.2.2.dist-info/licenses/LICENSE +661 -0
akgentic/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ """Akgentic namespace package.
2
+
3
+ This __init__.py file marks the akgentic directory as a namespace package,
4
+ allowing multiple packages to contribute to the akgentic namespace.
5
+
6
+ Do not add any code here. Each module (core, llm, team, tool, etc.) will
7
+ have its own subpackage that extends this namespace.
8
+ """
9
+
10
+ __path__ = __import__("pkgutil").extend_path(__path__, __name__)
@@ -0,0 +1,91 @@
1
+ """akgentic-tool public API."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ if TYPE_CHECKING:
8
+ from .knowledge_graph.models import KnowledgeGraphStateEvent as KnowledgeGraphStateEvent
9
+
10
+ # Submodules with their own __init__ files
11
+ from . import mcp, planning, sandbox, search, team, workspace # noqa: F401
12
+ from .core import ( # noqa: F401
13
+ COMMAND,
14
+ SYSTEM_PROMPT,
15
+ TOOL_CALL,
16
+ BaseToolParam,
17
+ Channels,
18
+ ToolCard,
19
+ ToolFactory,
20
+ )
21
+ from .errors import RetriableError, ToolObserverGone # noqa: F401
22
+ from .event import ( # noqa: F401
23
+ ActorToolObserver,
24
+ TeamManagementToolObserver,
25
+ ToolObserver,
26
+ ToolStateEvent,
27
+ ToolStatePayload,
28
+ )
29
+ from .sandbox.bwrap import BwrapSandboxActor # noqa: F401
30
+ from .sandbox.seatbelt import SeatbeltSandboxActor # noqa: F401
31
+ from .sandbox.tool import ExecTool # noqa: F401
32
+ from .workspace.tool import WorkspaceTool # noqa: F401
33
+
34
+ try:
35
+ from .vector import EmbeddingService, VectorEntry, VectorIndex # noqa: F401
36
+
37
+ _VECTOR_SEARCH_AVAILABLE = True
38
+ except ImportError:
39
+ _VECTOR_SEARCH_AVAILABLE = False
40
+
41
+ __all__ = [
42
+ # Core abstractions
43
+ "BaseToolParam",
44
+ "ToolCard",
45
+ "ToolFactory",
46
+ # Expose channel constants
47
+ "COMMAND",
48
+ "SYSTEM_PROMPT",
49
+ "TOOL_CALL",
50
+ "Channels",
51
+ # Errors
52
+ "RetriableError",
53
+ "ToolObserverGone",
54
+ # Events and observers
55
+ "ToolObserver",
56
+ "ActorToolObserver",
57
+ "TeamManagementToolObserver",
58
+ "ToolStateEvent",
59
+ "ToolStatePayload",
60
+ "KnowledgeGraphStateEvent",
61
+ # Submodules
62
+ "mcp",
63
+ "planning",
64
+ "sandbox",
65
+ "search",
66
+ "team",
67
+ "workspace",
68
+ "BwrapSandboxActor",
69
+ "ExecTool",
70
+ "SeatbeltSandboxActor",
71
+ "WorkspaceTool",
72
+ ]
73
+
74
+ if _VECTOR_SEARCH_AVAILABLE:
75
+ __all__ += ["VectorEntry", "EmbeddingService", "VectorIndex"]
76
+
77
+
78
+ def __getattr__(name: str) -> Any:
79
+ """Lazy re-export of the KG delta payload (Story 17.1).
80
+
81
+ ``KnowledgeGraphStateEvent`` lives in ``akgentic.tool.knowledge_graph.models``
82
+ and pulls the ``[vector_search]`` optional dependency chain when imported.
83
+ Exposing it via module ``__getattr__`` keeps the bare ``akgentic.tool``
84
+ import cheap (see ``test_tool_import_does_not_trigger_kg_import``) while
85
+ still honoring AC #5 of Story 17.1.
86
+ """
87
+ if name == "KnowledgeGraphStateEvent":
88
+ from .knowledge_graph.models import KnowledgeGraphStateEvent
89
+
90
+ return KnowledgeGraphStateEvent
91
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
akgentic/tool/core.py ADDED
@@ -0,0 +1,377 @@
1
+ """Tool abstractions and factory for the akgentic tool package.
2
+
3
+ Defines the core contracts:
4
+ - ``BaseToolParam``: base for capability parameter models.
5
+ - ``ToolCard``: abstract base — tool configuration + callable factory in one class.
6
+ - ``ToolFactory``: resolves ``ToolCard`` instances into callable tools, prompts, and toolsets.
7
+ """
8
+
9
+ import functools
10
+ import weakref
11
+ from abc import ABC, abstractmethod
12
+ from collections import deque
13
+ from enum import StrEnum
14
+ from typing import Any, Callable, TypeVar
15
+
16
+ from pydantic import PrivateAttr
17
+
18
+ from akgentic.core.utils import SerializableBaseModel
19
+ from akgentic.tool.errors import RetriableError, ToolObserverGone
20
+ from akgentic.tool.event import ToolObserver
21
+
22
+ T = TypeVar("T", bound="BaseToolParam")
23
+
24
+
25
+ def _resolve(value: "T | bool", cls: "type[T]") -> "T | None":
26
+ """Resolve a ``ParamModel | bool`` field to a ``ParamModel`` or ``None``.
27
+
28
+ Args:
29
+ value: ``True`` (enable with defaults), ``False`` (disable), or a
30
+ ``BaseToolParam`` instance (enable with custom parameters).
31
+ cls: The param model class to instantiate when *value* is ``True``.
32
+
33
+ Returns:
34
+ A param model instance, or ``None`` if the capability is disabled.
35
+ """
36
+ if value is True:
37
+ return cls()
38
+ if value is False:
39
+ return None
40
+ return value # already a ParamModel instance
41
+
42
+
43
+ class Channels(StrEnum):
44
+ """Valid channel names for capability exposure."""
45
+
46
+ SYSTEM_PROMPT = "system_prompt"
47
+ """Expose as a system prompt injected into the LLM context."""
48
+
49
+ TOOL_CALL = "tool_call"
50
+ """Expose as a callable tool for the LLM."""
51
+
52
+ COMMAND = "command"
53
+ """Expose as a programmatic command for inter-agent orchestration."""
54
+
55
+
56
+ # Backward-compatible module-level aliases
57
+ SYSTEM_PROMPT = Channels.SYSTEM_PROMPT
58
+ TOOL_CALL = Channels.TOOL_CALL
59
+ COMMAND = Channels.COMMAND
60
+
61
+
62
+ class BaseToolParam(SerializableBaseModel):
63
+ """Base for capability parameter models.
64
+
65
+ Provides common fields that control how a capability is exposed
66
+ and how its description can be customized.
67
+
68
+ Each subclass can override the default ``expose`` set to declare the channels
69
+ it participates in. Use the module-level channel constants:
70
+
71
+ - ``TOOL_CALL``: callable tool invoked by the LLM (default).
72
+ - ``SYSTEM_PROMPT``: prompt injected into the LLM context.
73
+ - ``COMMAND``: programmatic call for inter-agent orchestration.
74
+ """
75
+
76
+ instructions: str | None = None
77
+ """Additional instructions appended to the default tool docstring.
78
+
79
+ When set, the factory appends these instructions to the built-in docstring
80
+ under a structured header. When ``None``, only the default docstring is used.
81
+ """
82
+
83
+ expose: set[Channels] = {TOOL_CALL}
84
+ """Set of channels this capability is exposed through.
85
+
86
+ Defaults to ``{TOOL_CALL}``. Override in subclasses or at instantiation.
87
+ Use ``Channels`` enum members or module-level aliases: ``TOOL_CALL``, ``SYSTEM_PROMPT``,
88
+ ``COMMAND``.
89
+ """
90
+
91
+ def format_docstring(self, original: str | None) -> str | None:
92
+ """Format the tool docstring with optional additional instructions.
93
+
94
+ Args:
95
+ original: The original docstring from the tool callable.
96
+
97
+ Returns:
98
+ The formatted docstring, or the original if no instructions are set.
99
+ """
100
+ if not self.instructions:
101
+ return original
102
+
103
+ base_doc = original or ""
104
+ return f"{base_doc}\n\nAdditional Instructions:\n{self.instructions}"
105
+
106
+
107
+ class ToolCard(SerializableBaseModel, ABC):
108
+ """Abstract base: tool configuration + callable factory in one class.
109
+
110
+ Subclasses define typed fields for their capabilities and implement
111
+ the factory methods that produce LLM-callable functions.
112
+
113
+ Attributes:
114
+ name: Human-readable tool provider name.
115
+ description: Natural-language description of tool capabilities.
116
+ """
117
+
118
+ name: str
119
+ description: str
120
+ # Runtime-only, WEAK: a tool/closure/registry must never pin its agent.
121
+ _observer_ref: "weakref.ref[ToolObserver] | None" = PrivateAttr(default=None)
122
+
123
+ @property
124
+ def depends_on(self) -> list[str]:
125
+ """Class-name list of ToolCards that MUST be wired before this one.
126
+
127
+ Default: no dependencies. Subclasses may override as a property
128
+ whose return value depends on instance fields (e.g. the value of
129
+ a ``vector_store`` field on consumer tools). The string is matched
130
+ against ``type(card).__name__`` by ``ToolFactory``'s topological
131
+ sort. Not a Pydantic field — does not appear in ``model_dump`` and
132
+ cannot be set via ``model_validate``.
133
+ """
134
+ return []
135
+
136
+ def observer(self, observer: ToolObserver) -> "ToolCard":
137
+ """Attach an observer (held weakly) and perform runtime setup.
138
+
139
+ Follows the same pattern as ``BaseState.observer()``.
140
+ Override for setup that requires the observer (e.g., actor proxies).
141
+ All methods can then access the observer via ``self._observer``.
142
+
143
+ The observer is stored through a ``weakref`` so a tool, its closures, and
144
+ its command registry can never pin a stopped owning agent in memory.
145
+
146
+ Args:
147
+ observer: Optional observer for tool call events.
148
+
149
+ Returns:
150
+ Self, enabling method chaining.
151
+ """
152
+ self._observer = observer
153
+ return self
154
+
155
+ @property
156
+ def _observer(self) -> "ToolObserver":
157
+ """Live observer for synchronous, in-life use. Raises if the agent has stopped."""
158
+ obs = self._observer_or_none()
159
+ if obs is None:
160
+ raise ToolObserverGone("tool used after its owning agent was stopped")
161
+ return obs
162
+
163
+ @_observer.setter
164
+ def _observer(self, observer: ToolObserver) -> None:
165
+ """Store the observer weakly (backward-compatible ``self._observer = observer``)."""
166
+ self._observer_ref = weakref.ref(observer)
167
+
168
+ def _observer_or_none(self) -> "ToolObserver | None":
169
+ """Return the live observer, or ``None`` if unset or already collected."""
170
+ return self._observer_ref() if self._observer_ref is not None else None
171
+
172
+ @abstractmethod
173
+ def get_tools(self) -> list[Callable]:
174
+ """Return callable tool functions for LLM agents.
175
+
176
+ Use ``self._observer`` when tool callables need to emit events.
177
+ """
178
+ ...
179
+
180
+ def get_system_prompts(self) -> list[Callable]:
181
+ """Return system prompt callables injected into LLM context.
182
+
183
+ Use ``self._observer`` when prompts need runtime data.
184
+ """
185
+ return []
186
+
187
+ def get_commands(self) -> dict[type["BaseToolParam"], Callable]:
188
+ """Return callable commands for programmatic invocation.
189
+
190
+ Commands are methods exposed for inter-agent orchestration
191
+ (e.g., ``hire_member``, ``fire_member``). Unlike tools (invoked by
192
+ the LLM), commands are called programmatically by other agents
193
+ or system components via ``proxy_call`` or similar mechanisms.
194
+
195
+ Returns:
196
+ Dict mapping param class (e.g., ``HireTeamMember``) to callable.
197
+ """
198
+ return {}
199
+
200
+ def get_toolsets(self) -> list[Any]:
201
+ """Return runtime toolset objects (e.g., MCP servers)."""
202
+ return []
203
+
204
+
205
+ def _topological_sort(cards: list[ToolCard]) -> list[ToolCard]:
206
+ """Return ``cards`` topologically sorted by ``ToolCard.depends_on``.
207
+
208
+ Dependency keys are matched against ``type(card).__name__``. The sort uses
209
+ Kahn's algorithm with a FIFO queue seeded in input order, which produces a
210
+ deterministic ordering: independent nodes retain their relative input order.
211
+
212
+ Duplicate class names in ``cards`` (e.g. two ``VectorStoreTool`` instances
213
+ with different configuration) are permitted — later entries overwrite
214
+ earlier entries in the internal name→card map. Dependency relationships
215
+ are at the class level, not per-instance.
216
+
217
+ Args:
218
+ cards: Tool cards to sort. Input order is preserved for independent
219
+ nodes.
220
+
221
+ Returns:
222
+ A new list containing the same cards in dependency-respecting order
223
+ (prerequisites before dependents).
224
+
225
+ Raises:
226
+ ValueError: If a declared dependency is not present in ``cards``
227
+ (message names both the dependent and the missing class), or if
228
+ the dependency graph contains a cycle (message contains ``"cycle"``
229
+ and lists the class names involved).
230
+ """
231
+ # Name → card map. Later duplicates overwrite earlier entries — dependency
232
+ # relationships are at the class level, not per-instance.
233
+ by_name: dict[str, ToolCard] = {type(card).__name__: card for card in cards}
234
+
235
+ # Validate every declared dependency is present.
236
+ for card in cards:
237
+ for dep in card.depends_on:
238
+ if dep not in by_name:
239
+ raise ValueError(
240
+ f"{type(card).__name__} depends on {dep} but it was not "
241
+ f"found in the tool list"
242
+ )
243
+
244
+ # Build in-degree map keyed by class name (not instance — duplicates collapse).
245
+ in_degree: dict[str, int] = {name: 0 for name in by_name}
246
+ # Reverse adjacency: dep_name → list of class names that depend on it.
247
+ dependents: dict[str, list[str]] = {name: [] for name in by_name}
248
+ for name, card in by_name.items():
249
+ for dep in card.depends_on:
250
+ in_degree[name] += 1
251
+ dependents[dep].append(name)
252
+
253
+ # Seed the queue with zero-in-degree names in the order they appeared in
254
+ # the input (FIFO → deterministic for the same input). We iterate cards to
255
+ # preserve input order, skipping duplicates.
256
+ queue: deque[str] = deque()
257
+ seen: set[str] = set()
258
+ for card in cards:
259
+ name = type(card).__name__
260
+ if name in seen:
261
+ continue
262
+ seen.add(name)
263
+ if in_degree[name] == 0:
264
+ queue.append(name)
265
+
266
+ ordered_names: list[str] = []
267
+ while queue:
268
+ name = queue.popleft()
269
+ ordered_names.append(name)
270
+ for dependent in dependents[name]:
271
+ in_degree[dependent] -= 1
272
+ if in_degree[dependent] == 0:
273
+ queue.append(dependent)
274
+
275
+ if len(ordered_names) < len(by_name):
276
+ remaining = sorted(set(by_name) - set(ordered_names))
277
+ raise ValueError(
278
+ f"ToolCard dependency cycle detected: {remaining}"
279
+ )
280
+
281
+ # Map sorted names back to ToolCard instances. Preserve input order for
282
+ # duplicate class names: emit instances in the order they appeared in the
283
+ # input, grouped by their class's position in the sorted name order.
284
+ by_name_instances: dict[str, list[ToolCard]] = {name: [] for name in by_name}
285
+ for card in cards:
286
+ by_name_instances[type(card).__name__].append(card)
287
+ ordered: list[ToolCard] = []
288
+ for name in ordered_names:
289
+ ordered.extend(by_name_instances[name])
290
+ return ordered
291
+
292
+
293
+ class ToolFactory:
294
+ """Resolves ``ToolCard`` instances into callable tools, prompts, and toolsets."""
295
+
296
+ def __init__(
297
+ self,
298
+ tool_cards: list[ToolCard],
299
+ observer: ToolObserver | None = None,
300
+ retry_exception: type[Exception] | None = None,
301
+ ) -> None:
302
+ """Create a factory for one or more tool cards.
303
+
304
+ Topologically sorts ``tool_cards`` by their ``depends_on`` class
305
+ attribute, then attaches the observer to every card in dependency order
306
+ (triggers runtime setup in ``ToolCard.observer()``). Prerequisites are
307
+ wired before dependents, so a consumer card's ``observer()`` can safely
308
+ look up actors or resources created by its prerequisites.
309
+
310
+ Args:
311
+ tool_cards: Tool cards to resolve into callable tools. The caller's
312
+ list is not mutated; a new dependency-ordered list is stored on
313
+ ``self.tool_cards``. Aggregators (``get_tools``,
314
+ ``get_system_prompts``, ``get_commands``, ``get_toolsets``)
315
+ iterate in this dependency order.
316
+ observer: Optional observer notified by tool implementations during
317
+ tool calls.
318
+ retry_exception: Optional exception class to raise when a tool raises
319
+ ``RetriableError``. Injected by the integration layer (e.g., ModelRetry
320
+ from pydantic-ai) to keep the tool module framework-agnostic.
321
+
322
+ Raises:
323
+ ValueError: If the dependency graph is invalid — either a card
324
+ declares ``depends_on`` for a class not present in
325
+ ``tool_cards``, or a cycle exists. Raised before any observer
326
+ is attached (fail fast at team creation).
327
+ """
328
+ self.tool_cards = _topological_sort(tool_cards)
329
+ self.observer = observer
330
+ self._retry_exception = retry_exception
331
+
332
+ if self.observer is not None:
333
+ for card in self.tool_cards:
334
+ card.observer(self.observer)
335
+
336
+ def _wrap_with_retry(self, fn: Callable) -> Callable:
337
+ """Wrap a tool callable to convert ``RetriableError`` into retry_exception."""
338
+ assert self._retry_exception is not None
339
+ retry_exc = self._retry_exception
340
+
341
+ @functools.wraps(fn)
342
+ def wrapper(*args, **kwargs):
343
+ try:
344
+ return fn(*args, **kwargs)
345
+ except RetriableError as e:
346
+ raise retry_exc(str(e)) from e
347
+
348
+ return wrapper
349
+
350
+ def get_tools(self) -> list[Callable]:
351
+ """Return tool callables aggregated from all tool cards."""
352
+ tools = [t for card in self.tool_cards for t in card.get_tools()]
353
+ if self._retry_exception is not None:
354
+ tools = [self._wrap_with_retry(t) for t in tools]
355
+ return tools
356
+
357
+ def get_system_prompts(self) -> list[Callable]:
358
+ """Return system prompt callables aggregated from all tool cards."""
359
+ return [p for card in self.tool_cards for p in card.get_system_prompts()]
360
+
361
+ def get_commands(self) -> dict[type[BaseToolParam], Callable]:
362
+ """Return command callables aggregated from all tool cards.
363
+
364
+ Returns:
365
+ Dict mapping param class to callable, merged from all tool cards.
366
+ """
367
+ commands: dict[type[BaseToolParam], Callable] = {}
368
+ for card in self.tool_cards:
369
+ commands.update(card.get_commands())
370
+
371
+ if self._retry_exception is not None:
372
+ commands = {k: self._wrap_with_retry(v) for k, v in commands.items()}
373
+ return commands
374
+
375
+ def get_toolsets(self) -> list[Any]:
376
+ """Return toolset instances aggregated from all tool cards."""
377
+ return [ts for card in self.tool_cards for ts in card.get_toolsets()]
@@ -0,0 +1,22 @@
1
+ """Tool-layer exceptions for error signaling.
2
+
3
+ These exceptions are framework-agnostic — the integration layer (e.g., akgentic-team)
4
+ translates them into the appropriate retry mechanism (e.g., pydantic-ai ModelRetry).
5
+ """
6
+
7
+
8
+ class RetriableError(Exception):
9
+ """Raised by tool implementations when the LLM should retry with corrected input.
10
+
11
+ Tools raise this to signal a recoverable error. The consuming framework
12
+ (via ToolFactory's retry_exception injection) converts it to the appropriate
13
+ retry mechanism.
14
+ """
15
+
16
+ pass
17
+
18
+
19
+ class ToolObserverGone(RuntimeError): # noqa: N818 — signals a lifecycle state, not a failure
20
+ """A tool callable ran after its owning agent was stopped."""
21
+
22
+ pass
akgentic/tool/event.py ADDED
@@ -0,0 +1,149 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from typing import TYPE_CHECKING, Protocol, TypeAlias, runtime_checkable
5
+
6
+ from akgentic.core.actor_address import ActorAddress
7
+ from akgentic.core.agent import AkgentType
8
+ from akgentic.core.messages import Message
9
+
10
+ if TYPE_CHECKING:
11
+ from akgentic.tool.knowledge_graph.models import KnowledgeGraphStateEvent
12
+
13
+ # Union of tool-specific delta payloads carried by ``ToolStateEvent`` (ADR-024).
14
+ # Defined as a ``TypeAlias`` so future stateful tools (e.g. ``VectorStoreStateEvent``)
15
+ # can extend the union without touching ``ToolStateEvent``. Uses a string forward
16
+ # reference to avoid the ``event.py → knowledge_graph.models`` import cycle.
17
+ ToolStatePayload: TypeAlias = "KnowledgeGraphStateEvent"
18
+
19
+
20
+ class ToolStateEvent(Message):
21
+ """Generic tool-state event envelope (ADR-024, Story 17.1).
22
+
23
+ Wraps a tool-specific delta payload so any stateful tool actor can broadcast
24
+ typed state changes on the existing orchestrator event stream. Inherits
25
+ ``team_id``, ``timestamp``, ``id``, ``sender``, and ``display_type`` from
26
+ :class:`akgentic.core.messages.Message` without override.
27
+
28
+ Attributes:
29
+ tool_id: Tool-actor name emitting the event (e.g. ``"#KnowledgeGraphTool"``).
30
+ seq: Per-tool monotonic sequence number (starts at 1, enforced in Story 17.2).
31
+ payload: Tool-specific delta payload (see :data:`ToolStatePayload`).
32
+ """
33
+
34
+ tool_id: str
35
+ seq: int
36
+ payload: ToolStatePayload
37
+
38
+
39
+ @runtime_checkable
40
+ class ToolObserver(Protocol):
41
+ """Basic observer protocol for tool interactions.
42
+
43
+ This protocol defines the minimal interface required for tools that only
44
+ need to emit events. Tools requiring actor-aware features should use
45
+ ActorToolObserver instead.
46
+ """
47
+
48
+ def notify_event(self, event: object) -> None:
49
+ """Called when a tool domain event is emitted.
50
+
51
+ Args:
52
+ event: Domain event object
53
+ """
54
+ ...
55
+
56
+
57
+ @runtime_checkable
58
+ class ActorToolObserver(ToolObserver, Protocol):
59
+ """Actor-aware observer protocol for tool interactions.
60
+
61
+ Extends ToolObserver with actor-specific capabilities needed by tools
62
+ that interact with the actor system (e.g., PlanningTool).
63
+ """
64
+
65
+ @property
66
+ def myAddress(self) -> ActorAddress: # noqa: N802
67
+ """Get the current actor's address."""
68
+ ...
69
+
70
+ @property
71
+ def orchestrator(self) -> ActorAddress | None:
72
+ """Get the orchestrator address."""
73
+ ...
74
+
75
+ @property
76
+ def team_id(self) -> uuid.UUID:
77
+ """Get the team id."""
78
+ ...
79
+
80
+ def proxy_ask(
81
+ self,
82
+ actor: ActorAddress,
83
+ actor_type: type[AkgentType] | None = None,
84
+ timeout: int | None = None,
85
+ ) -> AkgentType:
86
+ """Get a proxy to another actor.
87
+
88
+ Args:
89
+ actor: Address of the target actor
90
+ actor_type: Optional expected type of the target actor for better type checking
91
+ timeout: Optional timeout for the proxy ask
92
+
93
+ Returns:
94
+ Proxy object to interact with the target actor
95
+ """
96
+ ...
97
+
98
+
99
+ @runtime_checkable
100
+ class TeamManagementToolObserver(ActorToolObserver, Protocol):
101
+ """Observer protocol for team management tools.
102
+
103
+ Extends ActorToolObserver with team-specific capabilities needed by
104
+ TeamTool for hiring, firing, and managing team members within the
105
+ actor system.
106
+ """
107
+
108
+ def createActor( # noqa: N802
109
+ self,
110
+ actor_class: type[AkgentType],
111
+ *,
112
+ config: object,
113
+ ) -> ActorAddress:
114
+ """Create a child actor with the given config.
115
+
116
+ Args:
117
+ actor_class: The actor class to instantiate
118
+ config: Configuration object for the actor
119
+
120
+ Returns:
121
+ Address of the newly created actor
122
+ """
123
+ ...
124
+
125
+ def on_hire(self, address: ActorAddress) -> None:
126
+ """Hook called after hiring a team member.
127
+
128
+ Handles agent-specific concerns such as:
129
+ - Tracking child in agent's children list
130
+ - Updating local caches
131
+ - Any agent-specific bookkeeping
132
+
133
+ Args:
134
+ address: ActorAddress of hired agent
135
+ """
136
+ ...
137
+
138
+ def on_fire(self, address: ActorAddress) -> None:
139
+ """Hook called after firing a team member.
140
+
141
+ Handles agent-specific concerns such as:
142
+ - Removing from children tracking
143
+ - Clearing from local caches
144
+ - Any agent-specific cleanup
145
+
146
+ Args:
147
+ address: ActorAddress of fired agent
148
+ """
149
+ ...