agent-runtime-sdk 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 (51) hide show
  1. agent_runtime/__init__.py +84 -0
  2. agent_runtime/builder.py +317 -0
  3. agent_runtime/config/__init__.py +29 -0
  4. agent_runtime/config/definitions.py +144 -0
  5. agent_runtime/config/policies.py +63 -0
  6. agent_runtime/config/storage.py +117 -0
  7. agent_runtime/context.py +10 -0
  8. agent_runtime/definitions.py +33 -0
  9. agent_runtime/discovery.py +16 -0
  10. agent_runtime/exceptions.py +74 -0
  11. agent_runtime/mcp/__init__.py +28 -0
  12. agent_runtime/mcp/discovery.py +146 -0
  13. agent_runtime/mcp/metadata.py +68 -0
  14. agent_runtime/mcp/utils.py +52 -0
  15. agent_runtime/model_registry.py +40 -0
  16. agent_runtime/plugins/__init__.py +4 -0
  17. agent_runtime/plugins/base.py +90 -0
  18. agent_runtime/plugins/default.py +19 -0
  19. agent_runtime/plugins/instructions.py +38 -0
  20. agent_runtime/plugins/loader.py +59 -0
  21. agent_runtime/policies.py +15 -0
  22. agent_runtime/runtime.py +110 -0
  23. agent_runtime/runtime_engine/__init__.py +22 -0
  24. agent_runtime/runtime_engine/a2a_bridge.py +190 -0
  25. agent_runtime/runtime_engine/a2a_task_io.py +165 -0
  26. agent_runtime/runtime_engine/agent_build.py +315 -0
  27. agent_runtime/runtime_engine/context.py +469 -0
  28. agent_runtime/runtime_engine/loading.py +170 -0
  29. agent_runtime/runtime_engine/observability.py +154 -0
  30. agent_runtime/runtime_engine/policy_registry.py +98 -0
  31. agent_runtime/runtime_engine/protocol_tools.py +94 -0
  32. agent_runtime/runtime_engine/task_flow.py +897 -0
  33. agent_runtime/runtime_engine/tool_flow.py +332 -0
  34. agent_runtime/sdk_agent.py +548 -0
  35. agent_runtime/server/__init__.py +15 -0
  36. agent_runtime/server/app_factory.py +37 -0
  37. agent_runtime/server/bootstrap.py +48 -0
  38. agent_runtime/server/endpoint_utils.py +37 -0
  39. agent_runtime/server/management.py +107 -0
  40. agent_runtime/smol/__init__.py +4 -0
  41. agent_runtime/smol/agents.py +431 -0
  42. agent_runtime/smol/llm_models.py +212 -0
  43. agent_runtime/smol/memory.py +111 -0
  44. agent_runtime/smol/models.py +69 -0
  45. agent_runtime/standalone.py +57 -0
  46. agent_runtime/storage.py +5 -0
  47. agent_runtime/tools.py +5 -0
  48. agent_runtime_sdk-0.1.0.dist-info/METADATA +125 -0
  49. agent_runtime_sdk-0.1.0.dist-info/RECORD +51 -0
  50. agent_runtime_sdk-0.1.0.dist-info/WHEEL +5 -0
  51. agent_runtime_sdk-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,154 @@
1
+ """Langfuse/OpenTelemetry integration for the smolagents runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from contextlib import contextmanager
6
+ from dataclasses import dataclass
7
+ import logging
8
+ import os
9
+ from typing import Any, Iterator
10
+
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+ _FALSE_VALUES = {"0", "false", "no", "off", "disable", "disabled"}
15
+ _SMOLAGENTS_INSTRUMENTED = False
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class A2ATaskObservation:
20
+ agent_id: str
21
+ task_id: str
22
+ context_id: str | None
23
+ request_headers: dict[str, str]
24
+ task_input: str
25
+
26
+
27
+ def setup_smolagents_observability() -> None:
28
+ """Enable the official smolagents OpenTelemetry instrumentation when configured."""
29
+
30
+ global _SMOLAGENTS_INSTRUMENTED
31
+ if _SMOLAGENTS_INSTRUMENTED or not _langfuse_enabled():
32
+ return
33
+
34
+ try:
35
+ from langfuse import get_client
36
+ from openinference.instrumentation.smolagents import SmolagentsInstrumentor
37
+ except ImportError:
38
+ logger.info(
39
+ "Langfuse env configured but smolagents telemetry dependencies are not "
40
+ "installed; runtime observability disabled"
41
+ )
42
+ return
43
+
44
+ try:
45
+ get_client()
46
+ SmolagentsInstrumentor().instrument()
47
+ except Exception:
48
+ logger.warning("Failed to initialize smolagents observability", exc_info=True)
49
+ return
50
+
51
+ _SMOLAGENTS_INSTRUMENTED = True
52
+ logger.info("Smolagents observability instrumentation enabled")
53
+
54
+
55
+ @contextmanager
56
+ def a2a_task_observation(
57
+ observation: A2ATaskObservation | None,
58
+ ) -> Iterator[Any | None]:
59
+ """Create a thin A2A task span and propagate task metadata to smolagents spans."""
60
+
61
+ if observation is None or not _langfuse_enabled():
62
+ yield None
63
+ return
64
+
65
+ try:
66
+ from langfuse import get_client, propagate_attributes
67
+ except ImportError:
68
+ yield None
69
+ return
70
+
71
+ langfuse = get_client()
72
+ metadata = _task_metadata(observation)
73
+ user_email = observation.request_headers.get("user_email") or None
74
+ session_id = observation.request_headers.get("X-Session-Id") or None
75
+
76
+ span_cm = langfuse.start_as_current_observation(
77
+ as_type="span",
78
+ name="a2a.task",
79
+ input={"text": observation.task_input},
80
+ metadata=metadata,
81
+ )
82
+ propagate_cm = propagate_attributes(
83
+ user_id=user_email,
84
+ session_id=session_id,
85
+ metadata=metadata,
86
+ trace_name="a2a.task",
87
+ tags=["a2a", observation.agent_id],
88
+ )
89
+
90
+ try:
91
+ with span_cm as span:
92
+ with propagate_cm:
93
+ try:
94
+ yield span
95
+ except Exception as exc:
96
+ _update_observation_error(span, exc)
97
+ raise
98
+ finally:
99
+ _flush_langfuse(langfuse)
100
+
101
+
102
+ def update_observation_output(span: Any | None, output: Any) -> None:
103
+ if span is None:
104
+ return
105
+ try:
106
+ span.update(output=output)
107
+ except Exception:
108
+ logger.debug("Failed to update Langfuse observation output", exc_info=True)
109
+
110
+
111
+ def _langfuse_enabled() -> bool:
112
+ enabled = os.getenv("MCP_AGENT_LANGFUSE_ENABLED")
113
+ if enabled is not None and enabled.strip().lower() in _FALSE_VALUES:
114
+ return False
115
+ return all(
116
+ os.getenv(name)
117
+ for name in (
118
+ "LANGFUSE_PUBLIC_KEY",
119
+ "LANGFUSE_SECRET_KEY",
120
+ "LANGFUSE_BASE_URL",
121
+ )
122
+ )
123
+
124
+
125
+ def _task_metadata(observation: A2ATaskObservation) -> dict[str, str]:
126
+ metadata = {
127
+ "agent_id": observation.agent_id,
128
+ "task_id": observation.task_id,
129
+ "context_id": observation.context_id or "",
130
+ "user_email": observation.request_headers.get("user_email", ""),
131
+ "session_id": observation.request_headers.get("X-Session-Id", ""),
132
+ }
133
+ return {key: value for key, value in metadata.items() if value}
134
+
135
+
136
+ def _update_observation_error(span: Any, error: Exception) -> None:
137
+ try:
138
+ span.update(
139
+ output={
140
+ "error_type": type(error).__name__,
141
+ "message": str(error),
142
+ },
143
+ level="ERROR",
144
+ status_message=str(error),
145
+ )
146
+ except Exception:
147
+ logger.debug("Failed to update Langfuse observation error", exc_info=True)
148
+
149
+
150
+ def _flush_langfuse(langfuse: Any) -> None:
151
+ try:
152
+ langfuse.flush()
153
+ except Exception:
154
+ logger.debug("Failed to flush Langfuse client", exc_info=True)
@@ -0,0 +1,98 @@
1
+ from __future__ import annotations
2
+
3
+ """MCP 级工具治理的集中读写逻辑。
4
+
5
+ 这一层的目标是把 “tool policy 应该写到哪个 MCP 上” 这件事收敛到一个地方,
6
+ 避免 runtime 其他模块到处自己猜。
7
+ """
8
+
9
+ from typing import Any
10
+
11
+ from ..config.definitions import MCPSettings, ToolPolicy
12
+
13
+
14
+ class MCPPolicyRegistry:
15
+ """围绕 MCPSettings.tool_policies 提供统一的查询与合并接口。"""
16
+
17
+ def __init__(
18
+ self, mcps: list[MCPSettings], tool_source_by_name: dict[str, str] | None = None
19
+ ):
20
+ self._mcps = mcps
21
+ self._tool_source_by_name = tool_source_by_name or {}
22
+
23
+ def find_mcp(self, mcp_name: str) -> MCPSettings | None:
24
+ return next((mcp for mcp in self._mcps if mcp.name == mcp_name), None)
25
+
26
+ def resolve_policy_mcp(
27
+ self, tool_name: str, mcp_name: str | None = None
28
+ ) -> MCPSettings | None:
29
+ if mcp_name is not None:
30
+ return self.find_mcp(mcp_name)
31
+
32
+ configured = [mcp for mcp in self._mcps if tool_name in mcp.tool_policies]
33
+ if len(configured) == 1:
34
+ return configured[0]
35
+ if len(configured) > 1:
36
+ return configured[0]
37
+
38
+ source_mcp = self._tool_source_by_name.get(tool_name)
39
+ if source_mcp:
40
+ return self.find_mcp(source_mcp)
41
+ if len(self._mcps) == 1:
42
+ return self._mcps[0]
43
+ return None
44
+
45
+ def set_tool_policy(
46
+ self, tool_name: str, policy: ToolPolicy, mcp_name: str | None = None
47
+ ) -> None:
48
+ target_mcp = self.resolve_policy_mcp(tool_name, mcp_name)
49
+ if target_mcp is None:
50
+ raise ValueError(f"cannot resolve MCP for tool '{tool_name}'")
51
+ target_mcp.tool_policies[tool_name] = policy
52
+
53
+ def get_tool_policy(
54
+ self, tool_name: str, mcp_name: str | None = None
55
+ ) -> ToolPolicy | None:
56
+ target_mcp = self.resolve_policy_mcp(tool_name, mcp_name)
57
+ if target_mcp is None:
58
+ return None
59
+ return target_mcp.tool_policies.get(tool_name)
60
+
61
+ def list_tool_policies(self, mcp_name: str | None = None) -> dict[str, ToolPolicy]:
62
+ if mcp_name is not None:
63
+ target_mcp = self.find_mcp(mcp_name)
64
+ return {} if target_mcp is None else dict(target_mcp.tool_policies)
65
+
66
+ policies: dict[str, ToolPolicy] = {}
67
+ for mcp in self._mcps:
68
+ for tool_name, policy in mcp.tool_policies.items():
69
+ policies[tool_name] = policy
70
+ return policies
71
+
72
+ def remove_tool_policy(self, tool_name: str, mcp_name: str | None = None) -> bool:
73
+ target_mcp = self.resolve_policy_mcp(tool_name, mcp_name)
74
+ if target_mcp is None:
75
+ return False
76
+ return target_mcp.tool_policies.pop(tool_name, None) is not None
77
+
78
+ def is_tool_enabled(self, tool_name: str, mcp_name: str | None = None) -> bool:
79
+ policy = self.get_tool_policy(tool_name, mcp_name)
80
+ return True if policy is None else policy.enabled
81
+
82
+ def merged_policy(
83
+ self, tool_name: str, decision: Any, mcp_name: str | None = None
84
+ ) -> ToolPolicy:
85
+ policy = (self.get_tool_policy(tool_name, mcp_name) or ToolPolicy()).model_copy(
86
+ deep=True
87
+ )
88
+ if decision is None:
89
+ return policy
90
+ if decision.requires_confirmation is not None:
91
+ policy.requires_confirmation = decision.requires_confirmation
92
+ if decision.input_fields:
93
+ policy.input_fields = decision.input_fields
94
+ if decision.prompt is not None:
95
+ policy.prompt = decision.prompt
96
+ if decision.allow_arg_override is not None:
97
+ policy.allow_arg_override = decision.allow_arg_override
98
+ return policy
@@ -0,0 +1,94 @@
1
+ """运行时协议工具。
2
+
3
+ 这三个工具不是业务 MCP 工具,而是 runtime 和上层协议之间的桥梁:
4
+
5
+ - `ask_user`: 主动向用户追问缺失信息
6
+ - `ask_auth`: 向用户请求授权确认
7
+ - `final_answer`: 统一输出最终结果
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from .context import current_task_id, current_task_pool
15
+
16
+ try:
17
+ from smolagents import Tool
18
+ except (
19
+ ImportError
20
+ ): # pragma: no cover - fallback for local static tests without smolagents
21
+
22
+ class Tool: # type: ignore[no-redef]
23
+ name = ""
24
+ description = ""
25
+ inputs: dict[str, Any] = {}
26
+ output_type = "string"
27
+
28
+ def __init__(self):
29
+ pass
30
+
31
+
32
+ def _consume_task_input(input_key: str) -> Any:
33
+ try:
34
+ task_id = current_task_id.get()
35
+ pool = current_task_pool.get()
36
+ except LookupError:
37
+ return ""
38
+
39
+ task_info = pool.get(task_id)
40
+ if not task_info or not hasattr(task_info, input_key):
41
+ return ""
42
+
43
+ value = getattr(task_info, input_key)
44
+ setattr(task_info, input_key, None)
45
+ return value or ""
46
+
47
+
48
+ class AskUserTool(Tool):
49
+ name = "ask_user"
50
+ description = "向用户提出问题,补充信息。"
51
+ inputs = {
52
+ "task": {
53
+ "type": "string",
54
+ "description": "向用户表达你需要什么信息",
55
+ }
56
+ }
57
+ output_type = "string"
58
+
59
+ def forward(self, task: str):
60
+ return _consume_task_input("user_input")
61
+
62
+
63
+ class AskAuthTool(Tool):
64
+ name = "ask_auth"
65
+ description = (
66
+ "向用户请求授权确认。用户端返回布尔值:true 表示确认,false 表示拒绝。"
67
+ )
68
+ inputs = {
69
+ "task": {
70
+ "type": "string",
71
+ "description": "向用户说明需要什么授权信息",
72
+ }
73
+ }
74
+ output_type = "string"
75
+
76
+ def forward(self, task: str):
77
+ return _consume_task_input("auth_required")
78
+
79
+
80
+ class FinalAnswerTool(Tool):
81
+ name = "final_answer"
82
+ description = (
83
+ "agent 最后通过此工具输出最终结果,请确保最后一次调用的工具是 final_answer"
84
+ )
85
+ inputs = {
86
+ "answer": {
87
+ "type": "string",
88
+ "description": "最终结果内容",
89
+ }
90
+ }
91
+ output_type = "string"
92
+
93
+ def forward(self, answer: str):
94
+ return answer