dagent-ai 0.2.1__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 (72) hide show
  1. dagent/__init__.py +90 -0
  2. dagent/agent.py +89 -0
  3. dagent/capabilities/__init__.py +42 -0
  4. dagent/capabilities/bootstrap.py +28 -0
  5. dagent/capabilities/boundaries.py +32 -0
  6. dagent/capabilities/catalog.py +131 -0
  7. dagent/capabilities/decorator.py +207 -0
  8. dagent/capabilities/mcp/__init__.py +103 -0
  9. dagent/capabilities/mcp/config.py +53 -0
  10. dagent/capabilities/mcp/errors.py +27 -0
  11. dagent/capabilities/mcp/handlers.py +100 -0
  12. dagent/capabilities/mcp/manager.py +115 -0
  13. dagent/capabilities/mcp/schema.py +86 -0
  14. dagent/capabilities/mcp/server_task.py +96 -0
  15. dagent/capabilities/providers.py +431 -0
  16. dagent/capabilities/skills.py +700 -0
  17. dagent/capabilities/tools/__init__.py +6 -0
  18. dagent/capabilities/tools/boundary.py +147 -0
  19. dagent/capabilities/tools/command_tools.py +74 -0
  20. dagent/capabilities/tools/file_tools.py +110 -0
  21. dagent/capabilities/tools/registry.py +65 -0
  22. dagent/capabilities/toolsets.py +233 -0
  23. dagent/capabilities/workspace.py +27 -0
  24. dagent/config.py +82 -0
  25. dagent/dag_builder.py +375 -0
  26. dagent/harness_runtime/__init__.py +60 -0
  27. dagent/harness_runtime/artifacts.py +187 -0
  28. dagent/harness_runtime/capability_executor.py +82 -0
  29. dagent/harness_runtime/capability_scope.py +16 -0
  30. dagent/harness_runtime/dag_agent.py +1257 -0
  31. dagent/harness_runtime/dag_builder.py +544 -0
  32. dagent/harness_runtime/dag_executor.py +615 -0
  33. dagent/harness_runtime/feedback_learner.py +39 -0
  34. dagent/harness_runtime/profiled_agent.py +74 -0
  35. dagent/harness_runtime/runtime.py +585 -0
  36. dagent/harness_runtime/runtime_events.py +100 -0
  37. dagent/harness_runtime/runtime_session.py +123 -0
  38. dagent/harness_runtime/task_record.py +131 -0
  39. dagent/harness_runtime/tool_agent.py +699 -0
  40. dagent/harness_runtime/validator_agent.py +95 -0
  41. dagent/profiles.py +90 -0
  42. dagent/providers/__init__.py +15 -0
  43. dagent/providers/base.py +43 -0
  44. dagent/providers/mock.py +37 -0
  45. dagent/providers/openai_compatible.py +161 -0
  46. dagent/resources/__init__.py +1 -0
  47. dagent/resources/profiles/__init__.py +1 -0
  48. dagent/resources/profiles/conversation.md +23 -0
  49. dagent/resources/profiles/dag_agent.md +107 -0
  50. dagent/resources/profiles/feedback_learner.md +10 -0
  51. dagent/resources/profiles/validator_agent.md +43 -0
  52. dagent/result.py +353 -0
  53. dagent/review.py +91 -0
  54. dagent/runner.py +1161 -0
  55. dagent/schemas/__init__.py +71 -0
  56. dagent/schemas/artifact.py +30 -0
  57. dagent/schemas/capability.py +106 -0
  58. dagent/schemas/common.py +38 -0
  59. dagent/schemas/dag.py +88 -0
  60. dagent/schemas/edge.py +12 -0
  61. dagent/schemas/feedback.py +22 -0
  62. dagent/schemas/node.py +50 -0
  63. dagent/schemas/results.py +62 -0
  64. dagent/schemas/run_trace.py +176 -0
  65. dagent/schemas/value.py +105 -0
  66. dagent/state/__init__.py +6 -0
  67. dagent/state/prompt_builder.py +64 -0
  68. dagent_ai-0.2.1.dist-info/METADATA +421 -0
  69. dagent_ai-0.2.1.dist-info/RECORD +72 -0
  70. dagent_ai-0.2.1.dist-info/WHEEL +5 -0
  71. dagent_ai-0.2.1.dist-info/licenses/LICENSE +201 -0
  72. dagent_ai-0.2.1.dist-info/top_level.txt +1 -0
dagent/__init__.py ADDED
@@ -0,0 +1,90 @@
1
+ """Reviewable DAG agent runtime SDK."""
2
+
3
+ from dagent.agent import AutoAgent, DagAgent, ToolAgent
4
+ from dagent.capabilities import (
5
+ SkillAmbiguousError,
6
+ SkillEntry,
7
+ SkillNotFoundError,
8
+ SkillPermissionError,
9
+ SkillStore,
10
+ SkillStoreError,
11
+ SkillView,
12
+ default_managed_skill_root,
13
+ default_skill_roots,
14
+ )
15
+ from dagent.capabilities.decorator import CapabilityBinding, tool
16
+ from dagent.dag_builder import ArtifactRef, ArtifactValueRef, Dag, FormatRef, InputRef, Node, NodeOutputRef
17
+ from dagent.harness_runtime import ArtifactUpload, CapabilityScope, validate_dag_spec
18
+ from dagent.profiles import AgentProfile, ProfileStore, list_builtin_profiles, load_builtin_profile
19
+ from dagent.providers import Provider
20
+ from dagent.result import RunResult, RunStreamChunk, RunStreamEvent
21
+ from dagent.review import ReviewDecision, ReviewHandle, ReviewLevel
22
+ from dagent.runner import Runner
23
+ from dagent.schemas import (
24
+ Boundary,
25
+ CapabilityDefinition,
26
+ CapabilityInvocation,
27
+ CapabilityPolicy,
28
+ CapabilityResult,
29
+ DAG,
30
+ DAGRun,
31
+ DAGSpec,
32
+ PendingReview,
33
+ RiskLevel,
34
+ RunTrace,
35
+ RuntimeResponse,
36
+ )
37
+
38
+ __version__ = "0.2.1"
39
+
40
+ __all__ = [
41
+ "__version__",
42
+ "AgentProfile",
43
+ "ArtifactRef",
44
+ "ArtifactUpload",
45
+ "ArtifactValueRef",
46
+ "AutoAgent",
47
+ "Boundary",
48
+ "CapabilityBinding",
49
+ "CapabilityDefinition",
50
+ "CapabilityInvocation",
51
+ "CapabilityPolicy",
52
+ "CapabilityResult",
53
+ "CapabilityScope",
54
+ "DAG",
55
+ "DAGRun",
56
+ "DAGSpec",
57
+ "DagAgent",
58
+ "Dag",
59
+ "FormatRef",
60
+ "InputRef",
61
+ "list_builtin_profiles",
62
+ "load_builtin_profile",
63
+ "Node",
64
+ "NodeOutputRef",
65
+ "PendingReview",
66
+ "ProfileStore",
67
+ "Provider",
68
+ "ReviewLevel",
69
+ "ReviewDecision",
70
+ "ReviewHandle",
71
+ "RiskLevel",
72
+ "RunResult",
73
+ "RunStreamChunk",
74
+ "RunStreamEvent",
75
+ "RunTrace",
76
+ "RuntimeResponse",
77
+ "Runner",
78
+ "SkillAmbiguousError",
79
+ "SkillEntry",
80
+ "SkillNotFoundError",
81
+ "SkillPermissionError",
82
+ "SkillStore",
83
+ "SkillStoreError",
84
+ "SkillView",
85
+ "ToolAgent",
86
+ "default_managed_skill_root",
87
+ "default_skill_roots",
88
+ "tool",
89
+ "validate_dag_spec",
90
+ ]
dagent/agent.py ADDED
@@ -0,0 +1,89 @@
1
+ """Declarative public agent SDK configurations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Iterable
8
+
9
+ from dagent.capabilities.decorator import CapabilityBinding
10
+ from dagent.profiles import AgentProfile
11
+ from dagent.review import ReviewLevel
12
+
13
+
14
+ CapabilityRef = CapabilityBinding | str
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class AutoAgent:
19
+ """Agent configuration that lets the runtime choose tool-loop or DAG execution."""
20
+
21
+ profile: str | AgentProfile = "conversation"
22
+ planner_profile: str | AgentProfile = "dag_agent"
23
+ name: str | None = None
24
+ max_steps: int = 8
25
+ max_cycles: int = 6
26
+ capabilities: Iterable[CapabilityRef] | None = None
27
+ skills: Iterable[str] | None = None
28
+ review: ReviewLevel = "fast"
29
+
30
+ def __post_init__(self) -> None:
31
+ object.__setattr__(self, "name", self.name or _default_profile_name(self.profile))
32
+ if self.max_steps < 1:
33
+ raise ValueError("max_steps must be at least 1.")
34
+ if self.max_cycles < 1:
35
+ raise ValueError("max_cycles must be at least 1.")
36
+ if self.capabilities is not None:
37
+ object.__setattr__(self, "capabilities", tuple(self.capabilities))
38
+ if self.skills is not None:
39
+ object.__setattr__(self, "skills", tuple(str(skill) for skill in self.skills))
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class ToolAgent:
44
+ """Profile-backed tool-loop agent configuration."""
45
+
46
+ profile: str | AgentProfile
47
+ name: str | None = None
48
+ max_steps: int = 8
49
+ capabilities: Iterable[CapabilityRef] | None = None
50
+ skills: Iterable[str] | None = None
51
+ review: ReviewLevel = "fast"
52
+ description: str = ""
53
+
54
+ def __post_init__(self) -> None:
55
+ object.__setattr__(self, "name", self.name or _default_profile_name(self.profile))
56
+ if self.max_steps < 1:
57
+ raise ValueError("max_steps must be at least 1.")
58
+ if self.capabilities is not None:
59
+ object.__setattr__(self, "capabilities", tuple(self.capabilities))
60
+ if self.skills is not None:
61
+ object.__setattr__(self, "skills", tuple(str(skill) for skill in self.skills))
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class DagAgent:
66
+ """Dynamic DAG planner configuration."""
67
+
68
+ planner_profile: str | AgentProfile = "dag_agent"
69
+ name: str | None = None
70
+ max_cycles: int = 6
71
+ capabilities: Iterable[CapabilityRef] | None = None
72
+ skills: Iterable[str] | None = None
73
+ review: ReviewLevel = "fast"
74
+
75
+ def __post_init__(self) -> None:
76
+ object.__setattr__(self, "name", self.name or _default_profile_name(self.planner_profile))
77
+ if self.max_cycles < 1:
78
+ raise ValueError("max_cycles must be at least 1.")
79
+ if self.capabilities is not None:
80
+ object.__setattr__(self, "capabilities", tuple(self.capabilities))
81
+ if self.skills is not None:
82
+ object.__setattr__(self, "skills", tuple(str(skill) for skill in self.skills))
83
+
84
+
85
+ def _default_profile_name(profile: str | AgentProfile) -> str:
86
+ if isinstance(profile, AgentProfile):
87
+ return profile.name
88
+ path = Path(profile)
89
+ return path.stem if path.suffix == ".md" else path.name
@@ -0,0 +1,42 @@
1
+ """Capability registration and providers."""
2
+
3
+ from dagent.capabilities.bootstrap import create_default_capability_catalog
4
+ from dagent.capabilities.catalog import CapabilityCatalog
5
+ from dagent.capabilities.decorator import CapabilityBinding, tool
6
+ from dagent.capabilities.mcp import MCPCapabilityProvider
7
+ from dagent.capabilities.providers import AgentCapabilityProvider, AgentNodeSessionStore
8
+ from dagent.capabilities.skills import (
9
+ SkillAmbiguousError,
10
+ SkillEntry,
11
+ SkillNotFoundError,
12
+ SkillPermissionError,
13
+ SkillStore,
14
+ SkillStoreError,
15
+ SkillView,
16
+ SkillsCapabilityProvider,
17
+ default_managed_skill_root,
18
+ default_skill_roots,
19
+ )
20
+ from dagent.capabilities.toolsets import CapabilityToolAdapter, CapabilityToolset
21
+
22
+ __all__ = [
23
+ "AgentCapabilityProvider",
24
+ "AgentNodeSessionStore",
25
+ "CapabilityBinding",
26
+ "CapabilityCatalog",
27
+ "CapabilityToolAdapter",
28
+ "CapabilityToolset",
29
+ "MCPCapabilityProvider",
30
+ "SkillAmbiguousError",
31
+ "SkillEntry",
32
+ "SkillNotFoundError",
33
+ "SkillPermissionError",
34
+ "SkillStore",
35
+ "SkillStoreError",
36
+ "SkillView",
37
+ "SkillsCapabilityProvider",
38
+ "create_default_capability_catalog",
39
+ "default_managed_skill_root",
40
+ "default_skill_roots",
41
+ "tool",
42
+ ]
@@ -0,0 +1,28 @@
1
+ """Default capability catalog assembly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from dagent.capabilities.catalog import CapabilityCatalog
8
+ from dagent.capabilities.providers import (
9
+ MemoryCapabilityProvider,
10
+ ToolCapabilityProvider,
11
+ )
12
+ from dagent.capabilities.skills import SkillsCapabilityProvider
13
+ from dagent.capabilities.tools.file_tools import create_file_tool_registry
14
+
15
+
16
+ def create_default_capability_catalog(
17
+ *,
18
+ workspace_root: str | Path = ".",
19
+ skill_roots: list[str | Path] | None = None,
20
+ skills_provider: SkillsCapabilityProvider | None = None,
21
+ ) -> CapabilityCatalog:
22
+ if skills_provider is not None and skill_roots is not None:
23
+ raise ValueError("Pass either skill_roots or skills_provider, not both.")
24
+ catalog = CapabilityCatalog(workspace_root=workspace_root)
25
+ ToolCapabilityProvider(create_file_tool_registry()).register_into(catalog)
26
+ MemoryCapabilityProvider().register_into(catalog)
27
+ (skills_provider or SkillsCapabilityProvider(skill_roots)).register_into(catalog)
28
+ return catalog
@@ -0,0 +1,32 @@
1
+ """Boundary inference for capability invocations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from dagent.schemas import Boundary, CapabilityDefinition
8
+
9
+
10
+ def infer_capability_boundary(
11
+ definition: CapabilityDefinition | None,
12
+ args: dict[str, Any],
13
+ ) -> Boundary:
14
+ if definition is None:
15
+ return Boundary(mode="read_only")
16
+ config = definition.config
17
+ checked_args = {**(config.get("default_args") or {}), **args}
18
+ path_args = tuple(config.get("path_args") or ())
19
+ paths = [_boundary_path_value(checked_args.get(path_arg)) for path_arg in path_args] or ["."]
20
+ action = str(config.get("action") or "read")
21
+
22
+ if action in {"write", "command"}:
23
+ return Boundary(mode="write_limited", allowed_paths=paths)
24
+ return Boundary(mode="read_only", allowed_paths=paths)
25
+
26
+
27
+ def _boundary_path_value(value: Any) -> Any:
28
+ if value is None or value == "":
29
+ return "."
30
+ if isinstance(value, dict):
31
+ return value
32
+ return str(value)
@@ -0,0 +1,131 @@
1
+ """Catalog for executable capabilities."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+
9
+ from dagent.schemas import (
10
+ CapabilityDefinition,
11
+ CapabilityInvocation,
12
+ CapabilityKind,
13
+ CapabilityResult,
14
+ )
15
+
16
+
17
+ CapabilityHandlerResult = CapabilityResult | Awaitable[CapabilityResult]
18
+ CapabilityHandler = Callable[..., CapabilityHandlerResult]
19
+ ShutdownHook = Callable[[], None]
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class CapabilityEntry:
24
+ definition: CapabilityDefinition
25
+ handler: CapabilityHandler
26
+ supports_context: bool = False
27
+
28
+
29
+ class CapabilityCatalog:
30
+ """Owns capability definitions and their executable handlers."""
31
+
32
+ def __init__(self, *, workspace_root: str | Path = ".") -> None:
33
+ self.workspace_root = Path(workspace_root).resolve()
34
+ self._entries: dict[str, CapabilityEntry] = {}
35
+ self._shutdown_hooks: list[ShutdownHook] = []
36
+ self._shutdown_complete = False
37
+
38
+ def register(
39
+ self,
40
+ definition: CapabilityDefinition,
41
+ handler: CapabilityHandler,
42
+ *,
43
+ supports_context: bool = False,
44
+ ) -> None:
45
+ if definition.id in self._entries:
46
+ raise ValueError(f"Capability '{definition.id}' is already registered.")
47
+ self._entries[definition.id] = CapabilityEntry(
48
+ definition=definition.model_copy(deep=True),
49
+ handler=handler,
50
+ supports_context=supports_context,
51
+ )
52
+
53
+ def replace(
54
+ self,
55
+ definition: CapabilityDefinition,
56
+ handler: CapabilityHandler,
57
+ *,
58
+ supports_context: bool = False,
59
+ ) -> None:
60
+ if definition.id not in self._entries:
61
+ raise KeyError(f"Capability '{definition.id}' is not registered.")
62
+ self._entries[definition.id] = CapabilityEntry(
63
+ definition=definition.model_copy(deep=True),
64
+ handler=handler,
65
+ supports_context=supports_context,
66
+ )
67
+
68
+ def set_enabled(self, capability_id: str, enabled: bool) -> CapabilityDefinition:
69
+ entry = self._entries.get(capability_id)
70
+ if entry is None:
71
+ raise KeyError(f"Capability '{capability_id}' is not registered.")
72
+ updated = entry.definition.model_copy(update={"enabled": enabled}, deep=True)
73
+ self._entries[capability_id] = CapabilityEntry(
74
+ definition=updated,
75
+ handler=entry.handler,
76
+ supports_context=entry.supports_context,
77
+ )
78
+ return updated
79
+
80
+ def delete(self, capability_id: str) -> None:
81
+ self._entries.pop(capability_id, None)
82
+
83
+ def get(self, capability_id: str) -> CapabilityDefinition | None:
84
+ entry = self._entries.get(capability_id)
85
+ return entry.definition.model_copy(deep=True) if entry is not None else None
86
+
87
+ def get_entry(self, capability_id: str) -> CapabilityEntry | None:
88
+ return self._entries.get(capability_id)
89
+
90
+ def get_by_name(self, name: str, *, kind: CapabilityKind | None = None) -> CapabilityDefinition | None:
91
+ for entry in self._entries.values():
92
+ definition = entry.definition
93
+ if definition.name == name and (kind is None or definition.kind == kind):
94
+ return definition.model_copy(deep=True)
95
+ return None
96
+
97
+ def list(self, *, kind: CapabilityKind | None = None, enabled_only: bool = False) -> list[CapabilityDefinition]:
98
+ definitions = [entry.definition for entry in self._entries.values()]
99
+ if kind is not None:
100
+ definitions = [definition for definition in definitions if definition.kind == kind]
101
+ if enabled_only:
102
+ definitions = [definition for definition in definitions if definition.enabled]
103
+ return sorted(
104
+ [definition.model_copy(deep=True) for definition in definitions],
105
+ key=lambda definition: definition.id,
106
+ )
107
+
108
+ def names(self, *, kind: CapabilityKind | None = None) -> set[str]:
109
+ return {definition.name for definition in self.list(kind=kind)}
110
+
111
+ def ids(self) -> set[str]:
112
+ return set(self._entries)
113
+
114
+ def add_shutdown_hook(self, hook: ShutdownHook) -> None:
115
+ if hook not in self._shutdown_hooks:
116
+ self._shutdown_hooks.append(hook)
117
+
118
+ def remove_shutdown_hook(self, hook: ShutdownHook) -> None:
119
+ try:
120
+ self._shutdown_hooks.remove(hook)
121
+ except ValueError:
122
+ pass
123
+
124
+ def shutdown(self) -> None:
125
+ if self._shutdown_complete:
126
+ return
127
+ self._shutdown_complete = True
128
+ hooks = list(self._shutdown_hooks)
129
+ self._shutdown_hooks.clear()
130
+ for hook in hooks:
131
+ hook()
@@ -0,0 +1,207 @@
1
+ """Public capability decorator for the dagent SDK."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import inspect
6
+ import json
7
+ from collections.abc import Callable
8
+ from dataclasses import dataclass
9
+ from typing import Any, get_type_hints
10
+
11
+ from pydantic import BaseModel
12
+
13
+ from dagent.capabilities.catalog import CapabilityHandler
14
+ from dagent.schemas import (
15
+ CapabilityDefinition,
16
+ CapabilityInvocation,
17
+ CapabilityPolicy,
18
+ CapabilityResult,
19
+ RiskLevel,
20
+ )
21
+ from dagent.schemas.common import json_schema_for_type
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CapabilityBinding:
26
+ """A public SDK binding of a capability definition and executable handler."""
27
+
28
+ definition: CapabilityDefinition
29
+ handler: CapabilityHandler
30
+ supports_context: bool = False
31
+
32
+
33
+ def tool(
34
+ fn: Callable[..., Any] | None = None,
35
+ *,
36
+ id: str | None = None,
37
+ name: str | None = None,
38
+ description: str = "",
39
+ risk: RiskLevel = "low",
40
+ requires_review: bool = False,
41
+ sandbox_required: bool = False,
42
+ network: bool = False,
43
+ secrets: list[str] | None = None,
44
+ parameters: dict[str, Any] | None = None,
45
+ config: dict[str, Any] | None = None,
46
+ enabled: bool = True,
47
+ supports_context: bool = False,
48
+ ) -> CapabilityBinding | Callable[[Callable[..., Any]], CapabilityBinding]:
49
+ """Decorate a Python function as a dagent tool capability.
50
+
51
+ This is the public way to expose a Python function as an LLM-callable
52
+ tool. MCP and skill capabilities are registered through
53
+ ``Runner.add_mcp_server`` and ``Runner.add_skill_root``.
54
+ """
55
+
56
+ def decorate(func: Callable[..., Any]) -> CapabilityBinding:
57
+ type_hints = _type_hints_for(func)
58
+ capability_name = name or func.__name__
59
+ capability_id = id or f"tool.{capability_name}"
60
+ definition = CapabilityDefinition(
61
+ id=capability_id,
62
+ name=capability_name,
63
+ kind="tool",
64
+ description=description or inspect.getdoc(func) or "",
65
+ parameters=parameters or _schema_from_signature(func, type_hints),
66
+ output_schema=_output_schema_from_signature(func, type_hints),
67
+ policy=CapabilityPolicy(
68
+ risk=risk,
69
+ requires_review=requires_review,
70
+ sandbox_required=sandbox_required,
71
+ network=network,
72
+ secrets=secrets or [],
73
+ ),
74
+ config=config or {},
75
+ enabled=enabled,
76
+ )
77
+
78
+ async def handler(
79
+ invocation: CapabilityInvocation,
80
+ *,
81
+ context: Any = None,
82
+ callbacks: Any = None,
83
+ ) -> CapabilityResult:
84
+ try:
85
+ result = _invoke_function(
86
+ func,
87
+ invocation.arguments,
88
+ context=context,
89
+ callbacks=callbacks,
90
+ supports_context=supports_context,
91
+ )
92
+ if inspect.isawaitable(result):
93
+ result = await result
94
+ if isinstance(result, CapabilityResult):
95
+ return _normalize_capability_result(result)
96
+ content, value = _content_and_value_from_result(result)
97
+ return CapabilityResult(
98
+ invocation_id=invocation.invocation_id,
99
+ capability_id=invocation.capability_id,
100
+ kind=invocation.kind,
101
+ status="completed",
102
+ content=content,
103
+ value=value,
104
+ )
105
+ except Exception as exc:
106
+ return CapabilityResult(
107
+ invocation_id=invocation.invocation_id,
108
+ capability_id=invocation.capability_id,
109
+ kind=invocation.kind,
110
+ status="failed",
111
+ error=str(exc),
112
+ stop_reason=type(exc).__name__,
113
+ )
114
+
115
+ return CapabilityBinding(
116
+ definition=definition,
117
+ handler=handler,
118
+ supports_context=supports_context,
119
+ )
120
+
121
+ if fn is not None:
122
+ return decorate(fn)
123
+ return decorate
124
+
125
+
126
+ def _invoke_function(
127
+ func: Callable[..., Any],
128
+ arguments: dict[str, Any],
129
+ *,
130
+ context: Any,
131
+ callbacks: Any,
132
+ supports_context: bool,
133
+ ) -> Any:
134
+ if supports_context:
135
+ return func(**arguments, context=context, callbacks=callbacks)
136
+ return func(**arguments)
137
+
138
+
139
+ def _content_and_value_from_result(result: Any) -> tuple[str, Any]:
140
+ if result is None:
141
+ return "", None
142
+ if isinstance(result, BaseModel):
143
+ value = result.model_dump(mode="json")
144
+ return result.model_dump_json(), value
145
+ if isinstance(result, str):
146
+ return result, result
147
+ if isinstance(result, bytes):
148
+ value = result.decode("utf-8", errors="replace")
149
+ return value, value
150
+ if isinstance(result, tuple):
151
+ value = list(result)
152
+ return json.dumps(value, ensure_ascii=False), value
153
+ if isinstance(result, (dict, list, bool, int, float)):
154
+ return json.dumps(result, ensure_ascii=False), result
155
+ value = str(result)
156
+ return value, value
157
+
158
+
159
+ def _normalize_capability_result(result: CapabilityResult) -> CapabilityResult:
160
+ if result.status == "completed" and result.value is None:
161
+ return result.model_copy(update={"value": result.content})
162
+ return result
163
+
164
+
165
+ def _schema_from_signature(func: Callable[..., Any], type_hints: dict[str, Any]) -> dict[str, Any]:
166
+ signature = inspect.signature(func)
167
+ properties: dict[str, Any] = {}
168
+ required: list[str] = []
169
+ for parameter in signature.parameters.values():
170
+ if parameter.kind in {
171
+ inspect.Parameter.VAR_POSITIONAL,
172
+ inspect.Parameter.VAR_KEYWORD,
173
+ }:
174
+ raise ValueError(f"Cannot infer schema for variadic parameter '{parameter.name}'.")
175
+ if parameter.name in {"context", "callbacks"}:
176
+ continue
177
+ schema = _schema_for_annotation(type_hints.get(parameter.name, parameter.annotation))
178
+ if parameter.default is inspect.Parameter.empty:
179
+ required.append(parameter.name)
180
+ else:
181
+ schema["default"] = parameter.default
182
+ properties[parameter.name] = schema
183
+ output: dict[str, Any] = {
184
+ "type": "object",
185
+ "properties": properties,
186
+ }
187
+ if required:
188
+ output["required"] = required
189
+ return output
190
+
191
+
192
+ def _output_schema_from_signature(func: Callable[..., Any], type_hints: dict[str, Any]) -> dict[str, Any]:
193
+ annotation = type_hints.get("return", inspect.signature(func).return_annotation)
194
+ return json_schema_for_type(annotation)
195
+
196
+
197
+ def _type_hints_for(func: Callable[..., Any]) -> dict[str, Any]:
198
+ try:
199
+ return get_type_hints(func, include_extras=True)
200
+ except (NameError, TypeError, AttributeError):
201
+ return {}
202
+
203
+
204
+ def _schema_for_annotation(annotation: Any) -> dict[str, Any]:
205
+ if annotation is inspect.Parameter.empty:
206
+ return {"type": "string"}
207
+ return json_schema_for_type(annotation) or {"type": "string"}