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,84 @@
1
+ from importlib.metadata import PackageNotFoundError, version
2
+
3
+ from .builder import AgentBuilder
4
+ from .definitions import (
5
+ A2ASettings,
6
+ AgentConfig,
7
+ AgentDefinition,
8
+ InputFieldDefinition,
9
+ MCPSettings,
10
+ ModelSettings,
11
+ PluginSettings,
12
+ RuntimeConfig,
13
+ SkillDefinition,
14
+ ToolPolicy,
15
+ )
16
+ from .discovery import DiscoveredTool
17
+ from .exceptions import (
18
+ AgentBuildError,
19
+ AgentRuntimeError,
20
+ DefinitionLoadError,
21
+ MCPToolLoadError,
22
+ PluginLoadError,
23
+ TaskCancelledError,
24
+ TaskWaitTimeoutError,
25
+ UserCancelledError,
26
+ )
27
+ from .model_registry import ModelRegistry
28
+ from .plugins.base import BaseAgentPlugin, BeforeToolDecision, ToolExecutionContext
29
+ from .plugins.default import DefaultPlugin
30
+ from .runtime import ManagedAgentRuntime
31
+ from .sdk_agent import (
32
+ BaseSDKAgent,
33
+ ConfiguredSDKAgent,
34
+ DeclarativeSDKAgent,
35
+ OpenAICompatibleSingleMCPAgent,
36
+ SingleMCPAgent,
37
+ )
38
+ from .server.bootstrap import load_single_agent_runtime
39
+ from .standalone import build_single_agent_app, run_single_agent_app
40
+ from .storage import AgentDefinitionStore
41
+
42
+ try:
43
+ __version__ = version("agent-runtime-sdk")
44
+ except PackageNotFoundError: # pragma: no cover - source tree without installed metadata
45
+ __version__ = "0.1.0"
46
+
47
+ __all__ = [
48
+ "__version__",
49
+ "A2ASettings",
50
+ "AgentConfig",
51
+ "AgentBuilder",
52
+ "AgentBuildError",
53
+ "AgentDefinition",
54
+ "AgentDefinitionStore",
55
+ "AgentRuntimeError",
56
+ "BaseAgentPlugin",
57
+ "BaseSDKAgent",
58
+ "ConfiguredSDKAgent",
59
+ "DeclarativeSDKAgent",
60
+ "BeforeToolDecision",
61
+ "DefinitionLoadError",
62
+ "DefaultPlugin",
63
+ "InputFieldDefinition",
64
+ "MCPToolLoadError",
65
+ "MCPSettings",
66
+ "ManagedAgentRuntime",
67
+ "ModelSettings",
68
+ "ModelRegistry",
69
+ "OpenAICompatibleSingleMCPAgent",
70
+ "PluginSettings",
71
+ "PluginLoadError",
72
+ "RuntimeConfig",
73
+ "SkillDefinition",
74
+ "SingleMCPAgent",
75
+ "TaskCancelledError",
76
+ "TaskWaitTimeoutError",
77
+ "DiscoveredTool",
78
+ "ToolExecutionContext",
79
+ "ToolPolicy",
80
+ "UserCancelledError",
81
+ "build_single_agent_app",
82
+ "load_single_agent_runtime",
83
+ "run_single_agent_app",
84
+ ]
@@ -0,0 +1,317 @@
1
+ from __future__ import annotations
2
+
3
+ """面向使用者的代码式构建入口。"""
4
+
5
+ import os
6
+ import re
7
+ from pathlib import Path
8
+ from typing import Any, Callable
9
+ from urllib.parse import urlparse
10
+
11
+ from .config.definitions import (
12
+ DEFAULT_PASS_THROUGH_HEADERS,
13
+ AgentConfig,
14
+ AgentDefinition,
15
+ InputFieldDefinition,
16
+ MCPSettings,
17
+ ModelSettings,
18
+ PluginSettings,
19
+ RuntimeConfig,
20
+ SkillDefinition,
21
+ ToolPolicy,
22
+ )
23
+ from .mcp.metadata import DiscoveredTool
24
+ from .plugins.base import BaseAgentPlugin
25
+ from .runtime import ManagedAgentRuntime
26
+
27
+
28
+ class AgentBuilder:
29
+ """代码式配置入口。"""
30
+
31
+ def __init__(self):
32
+ self._agent = AgentConfig(
33
+ agent_id="mcp_agent",
34
+ name="",
35
+ description="",
36
+ version="0.1.0",
37
+ )
38
+ self._runtime = RuntimeConfig(
39
+ base_path=".",
40
+ public_base_url=None,
41
+ model=ModelSettings(),
42
+ )
43
+ self._mcps: list[MCPSettings] = []
44
+
45
+ self._plugin_instance: BaseAgentPlugin | None = None
46
+ self._discoverer: Callable[..., list[DiscoveredTool]] | None = None
47
+ self._explicit_model_api_key: str | None = None
48
+ self._auto_agent_id = True
49
+
50
+ def model(
51
+ self,
52
+ *,
53
+ provider: str | None = None,
54
+ api_base: str | None = None,
55
+ model_id: str | None = None,
56
+ api_key: str | None = None,
57
+ api_key_env: str | None = None,
58
+ max_steps: int | None = None,
59
+ flatten_messages_as_text: bool | None = None,
60
+ ) -> AgentBuilder:
61
+ model = self._runtime.model
62
+ if provider is not None:
63
+ model.provider = provider
64
+ if api_base is not None:
65
+ model.api_base = api_base
66
+ if model_id is not None:
67
+ model.model_id = model_id
68
+ if api_key is not None:
69
+ self._explicit_model_api_key = api_key
70
+ if api_key_env is not None:
71
+ model.api_key_env = api_key_env
72
+ if max_steps is not None:
73
+ model.max_steps = max_steps
74
+ if flatten_messages_as_text is not None:
75
+ model.flatten_messages_as_text = flatten_messages_as_text
76
+ return self
77
+
78
+ def tool_policy(
79
+ self,
80
+ tool_name: str,
81
+ *,
82
+ mcp_name: str | None = None,
83
+ requires_confirmation: bool = False,
84
+ prompt: str | None = None,
85
+ enabled: bool = True,
86
+ allow_arg_override: bool = True,
87
+ input_fields: list[InputFieldDefinition | dict[str, Any]] | None = None,
88
+ ) -> AgentBuilder:
89
+ fields: list[InputFieldDefinition] = []
90
+ if input_fields:
91
+ for field in input_fields:
92
+ if isinstance(field, InputFieldDefinition):
93
+ fields.append(field)
94
+ else:
95
+ fields.append(InputFieldDefinition.model_validate(field))
96
+ target_mcp = self._resolve_policy_target_mcp(mcp_name)
97
+ target_mcp.tool_policies[tool_name] = ToolPolicy(
98
+ enabled=enabled,
99
+ requires_confirmation=requires_confirmation,
100
+ input_fields=fields,
101
+ prompt=prompt,
102
+ allow_arg_override=allow_arg_override,
103
+ )
104
+ return self
105
+
106
+ def plugin(self, plugin: BaseAgentPlugin) -> AgentBuilder:
107
+ self._plugin_instance = plugin
108
+ return self
109
+
110
+ def instructions(self, text: str) -> AgentBuilder:
111
+ self._agent.extra_instructions = text
112
+ return self
113
+
114
+ def max_steps(self, n: int) -> AgentBuilder:
115
+ self._runtime.model.max_steps = n
116
+ return self
117
+
118
+ def agent_info(
119
+ self,
120
+ *,
121
+ agent_id: str | None = None,
122
+ name: str | None = None,
123
+ description: str | None = None,
124
+ version: str | None = None,
125
+ ) -> AgentBuilder:
126
+ if agent_id is not None:
127
+ self._agent.agent_id = agent_id
128
+ self._auto_agent_id = False
129
+ if name is not None:
130
+ self._agent.name = name
131
+ if description is not None:
132
+ self._agent.description = description
133
+ if version is not None:
134
+ self._agent.version = version
135
+ return self
136
+
137
+ def add_mcp(
138
+ self,
139
+ *,
140
+ name: str,
141
+ url: str,
142
+ transport: str = "streamable-http",
143
+ enabled: bool = True,
144
+ retry_count: int = 3,
145
+ retry_delay_seconds: float = 1.0,
146
+ static_headers: dict[str, str] | None = None,
147
+ pass_through_headers: list[str] | None = None,
148
+ tool_policies: dict[str, ToolPolicy | dict[str, Any]] | None = None,
149
+ ) -> AgentBuilder:
150
+ policies: dict[str, ToolPolicy] = {}
151
+ if tool_policies:
152
+ for tool_name, policy in tool_policies.items():
153
+ policies[tool_name] = (
154
+ policy
155
+ if isinstance(policy, ToolPolicy)
156
+ else ToolPolicy.model_validate(policy)
157
+ )
158
+ self._mcps.append(
159
+ MCPSettings(
160
+ name=name,
161
+ url=url,
162
+ transport=transport,
163
+ enabled=enabled,
164
+ retry_count=retry_count,
165
+ retry_delay_seconds=retry_delay_seconds,
166
+ static_headers=static_headers or {},
167
+ pass_through_headers=pass_through_headers
168
+ or list(DEFAULT_PASS_THROUGH_HEADERS),
169
+ tool_policies=policies,
170
+ )
171
+ )
172
+ return self
173
+
174
+ def a2a_skill(
175
+ self,
176
+ *,
177
+ id: str,
178
+ name: str,
179
+ description: str,
180
+ tags: list[str] | None = None,
181
+ examples: list[str] | None = None,
182
+ ) -> AgentBuilder:
183
+ self._agent.a2a.skills.append(
184
+ SkillDefinition(
185
+ id=id,
186
+ name=name,
187
+ description=description,
188
+ tags=tags or [],
189
+ examples=examples or [],
190
+ )
191
+ )
192
+ return self
193
+
194
+ def public_base_url(self, url: str) -> AgentBuilder:
195
+ self._runtime.public_base_url = url
196
+ return self
197
+
198
+ def discoverer(self, func: Callable[..., list[DiscoveredTool]]) -> AgentBuilder:
199
+ self._discoverer = func
200
+ return self
201
+
202
+ def build(self) -> ManagedAgentRuntime:
203
+ definition = self.build_definition()
204
+ if self._explicit_model_api_key is not None:
205
+ os.environ[definition.runtime.model.api_key_env] = (
206
+ self._explicit_model_api_key
207
+ )
208
+
209
+ runtime = ManagedAgentRuntime(
210
+ definition=definition,
211
+ public_base_url=definition.runtime.public_base_url or "",
212
+ discoverer=self._discoverer,
213
+ )
214
+ if self._plugin_instance is not None:
215
+ runtime.plugin = self._plugin_instance
216
+ runtime.reload(discover=True, skip_plugin_load=True)
217
+ else:
218
+ runtime.reload(discover=True)
219
+ if runtime.load_error:
220
+ raise RuntimeError(runtime.load_error)
221
+ return runtime
222
+
223
+ def build_a2a_app(
224
+ self, *, public_url: str | None = None, enable_management: bool = True
225
+ ):
226
+ from .server.app_factory import build_app_from_runtime
227
+
228
+ if public_url is not None:
229
+ self.public_base_url(public_url)
230
+ runtime = self.build()
231
+ return build_app_from_runtime(runtime, enable_management=enable_management)
232
+
233
+ @classmethod
234
+ def from_yaml(cls, path: str | Path) -> AgentBuilder:
235
+ from .config.storage import AgentDefinitionStore
236
+
237
+ resolved = Path(path).resolve()
238
+ definition = AgentDefinitionStore(resolved.parent).load_path(resolved)
239
+ return cls.from_definition(definition)
240
+
241
+ @classmethod
242
+ def from_definition(cls, definition: AgentDefinition) -> AgentBuilder:
243
+ builder = cls()
244
+ builder._agent = definition.agent.model_copy(deep=True)
245
+ builder._runtime = definition.runtime.model_copy(deep=True)
246
+ builder._mcps = [mcp.model_copy(deep=True) for mcp in definition.mcps]
247
+ builder._auto_agent_id = False
248
+ return builder
249
+
250
+ def build_definition(self) -> AgentDefinition:
251
+ """返回当前 builder 收敛后的 `AgentDefinition`。
252
+
253
+ 这个方法适合:
254
+
255
+ - 需要在真正 `build()` 前检查最终配置
256
+ - Python SDK 层希望把 YAML / 类配置统一收敛到同一个 definition 规格
257
+ """
258
+
259
+ if not self._mcps:
260
+ raise ValueError("at least one MCP server must be added before build()")
261
+
262
+ agent = self._agent.model_copy(deep=True)
263
+ runtime = self._runtime.model_copy(deep=True)
264
+ mcps = [mcp.model_copy(deep=True) for mcp in self._mcps]
265
+
266
+ if self._auto_agent_id:
267
+ agent.agent_id = self._auto_agent_id_from_mcps(mcps)
268
+ if not agent.name:
269
+ agent.name = agent.agent_id
270
+ if not agent.description:
271
+ agent.description = agent.name
272
+
273
+ if runtime.model.api_base is None:
274
+ runtime.model.api_base = os.getenv("MODEL_SOURCE_API_BASE")
275
+ if runtime.model.model_id is None:
276
+ runtime.model.model_id = os.getenv("MODEL_SOURCE_MODEL_ID")
277
+
278
+ if self._plugin_instance is not None:
279
+ runtime.plugin = None
280
+
281
+ return AgentDefinition(
282
+ agent=agent,
283
+ runtime=runtime,
284
+ mcps=mcps,
285
+ )
286
+
287
+ def _build_definition(self) -> AgentDefinition:
288
+ """兼容旧内部调用;新代码请优先使用 `build_definition()`。"""
289
+
290
+ return self.build_definition()
291
+
292
+ @staticmethod
293
+ def _auto_agent_id_from_mcps(mcps: list[MCPSettings]) -> str:
294
+ parsed = urlparse(mcps[0].url)
295
+ path_part = (
296
+ parsed.path.strip("/").split("/")[-1] if parsed.path.strip("/") else ""
297
+ )
298
+ if path_part:
299
+ normalized = re.sub(r"[^0-9a-zA-Z_]", "_", path_part)
300
+ return f"mcp_{normalized}" if normalized else "mcp_agent"
301
+ return "mcp_agent"
302
+
303
+ def _resolve_policy_target_mcp(self, mcp_name: str | None) -> MCPSettings:
304
+ if mcp_name is not None:
305
+ for mcp in self._mcps:
306
+ if mcp.name == mcp_name:
307
+ return mcp
308
+ raise ValueError(f"mcp '{mcp_name}' not found")
309
+ if len(self._mcps) == 1:
310
+ return self._mcps[0]
311
+ if not self._mcps:
312
+ raise ValueError(
313
+ "add an MCP before tool_policy(), or pass mcp_name after configuring MCPs"
314
+ )
315
+ raise ValueError(
316
+ "multiple MCPs configured; please specify mcp_name when setting tool_policy()"
317
+ )
@@ -0,0 +1,29 @@
1
+ """配置层内部实现。"""
2
+
3
+ from .definitions import (
4
+ A2ASettings,
5
+ AgentConfig,
6
+ AgentDefinition,
7
+ InputFieldDefinition,
8
+ MCPSettings,
9
+ ModelSettings,
10
+ PluginSettings,
11
+ RuntimeConfig,
12
+ SkillDefinition,
13
+ ToolPolicy,
14
+ )
15
+ from .storage import AgentDefinitionStore
16
+
17
+ __all__ = [
18
+ "A2ASettings",
19
+ "AgentConfig",
20
+ "AgentDefinition",
21
+ "AgentDefinitionStore",
22
+ "InputFieldDefinition",
23
+ "MCPSettings",
24
+ "ModelSettings",
25
+ "PluginSettings",
26
+ "RuntimeConfig",
27
+ "SkillDefinition",
28
+ "ToolPolicy",
29
+ ]
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ """框架的静态定义层。
4
+
5
+ 这个文件只负责“描述一个 agent runtime 长什么样”,不负责真正执行。
6
+ 调用链通常是:
7
+
8
+ 1. 先把 YAML / Builder 收敛成 AgentDefinition
9
+ 2. 再由 ManagedAgentRuntime 读取这些定义构造运行时
10
+ 3. 最后通过 standalone / builder 暴露成 A2A 服务
11
+ """
12
+
13
+ import re
14
+ from typing import Any
15
+
16
+ from pydantic import BaseModel, Field, field_validator, model_validator
17
+
18
+
19
+ DEFAULT_PASS_THROUGH_HEADERS = ["X-Session-Id", "accessToken", "user_email"]
20
+ DEFAULT_PLUGIN_MODULE_PATH = "agent_app/plugin.py"
21
+ DEFAULT_PLUGIN_CLASS_NAME = "Plugin"
22
+
23
+
24
+ class InputFieldDefinition(BaseModel):
25
+ """描述某个工具参数缺失时,需要向用户补采的字段。"""
26
+
27
+ name: str
28
+ type: str = "string"
29
+ description: str = ""
30
+ required: bool = True
31
+ default: Any = None
32
+ map_to_arg: str | None = None
33
+
34
+ @property
35
+ def target_arg(self) -> str:
36
+ return self.map_to_arg or self.name
37
+
38
+
39
+ class ToolPolicy(BaseModel):
40
+ """工具治理策略。"""
41
+
42
+ enabled: bool = True
43
+ requires_confirmation: bool = False
44
+ input_fields: list[InputFieldDefinition] = Field(default_factory=list)
45
+ prompt: str | None = None
46
+ allow_arg_override: bool = True
47
+
48
+
49
+ class ModelSettings(BaseModel):
50
+ """模型侧配置。"""
51
+
52
+ provider: str = "openai_compatible"
53
+ api_base: str | None = None
54
+ model_id: str | None = None
55
+ api_key_env: str = "MODEL_SOURCE_API_KEY"
56
+ max_steps: int = 15
57
+ flatten_messages_as_text: bool = True
58
+
59
+
60
+ class MCPSettings(BaseModel):
61
+ """单个 MCP 服务的连接定义。"""
62
+
63
+ name: str
64
+ url: str
65
+ transport: str = "streamable-http"
66
+ enabled: bool = True
67
+ retry_count: int = Field(default=3, ge=0)
68
+ retry_delay_seconds: float = Field(default=1.0, ge=0)
69
+ static_headers: dict[str, str] = Field(default_factory=dict)
70
+ pass_through_headers: list[str] = Field(default_factory=lambda: list(DEFAULT_PASS_THROUGH_HEADERS))
71
+ # TODO: tool policy 后续还可以继续细分,不一定永远只挂在 MCP server 下。
72
+ tool_policies: dict[str, ToolPolicy] = Field(default_factory=dict)
73
+
74
+
75
+ class PluginSettings(BaseModel):
76
+ """插件加载配置。"""
77
+
78
+ module_path: str = DEFAULT_PLUGIN_MODULE_PATH
79
+ class_name: str = DEFAULT_PLUGIN_CLASS_NAME
80
+ config: dict[str, Any] = Field(default_factory=dict)
81
+
82
+
83
+ class SkillDefinition(BaseModel):
84
+ """A2A agent card 中对外暴露的一个技能描述。"""
85
+
86
+ id: str
87
+ name: str
88
+ description: str
89
+ tags: list[str] = Field(default_factory=list)
90
+ examples: list[str] = Field(default_factory=list)
91
+
92
+
93
+ class A2ASettings(BaseModel):
94
+ """A2A 展示层配置。"""
95
+
96
+ skills: list[SkillDefinition] = Field(default_factory=list)
97
+ default_input_modes: list[str] = Field(default_factory=lambda: ["text"])
98
+ default_output_modes: list[str] = Field(default_factory=lambda: ["text"])
99
+
100
+
101
+ class AgentConfig(BaseModel):
102
+ """Agent 的身份与展示配置。"""
103
+
104
+ agent_id: str
105
+ name: str
106
+ description: str
107
+ version: str = "0.1.0"
108
+ a2a: A2ASettings = Field(default_factory=A2ASettings)
109
+ extra_instructions: str = ""
110
+
111
+ @field_validator("agent_id")
112
+ @classmethod
113
+ def validate_agent_id(cls, value: str) -> str:
114
+ if not re.fullmatch(r"[a-zA-Z0-9][a-zA-Z0-9_-]*", value):
115
+ raise ValueError("agent_id must match [a-zA-Z0-9][a-zA-Z0-9_-]*")
116
+ return value
117
+
118
+
119
+ class RuntimeConfig(BaseModel):
120
+ """运行时必要配置。"""
121
+
122
+ base_path: str = "."
123
+ public_base_url: str | None = None
124
+ model: ModelSettings
125
+ plugin: PluginSettings | None = None
126
+
127
+
128
+ class AgentDefinition(BaseModel):
129
+ """一个 agent runtime 的完整静态定义。"""
130
+
131
+ agent: AgentConfig
132
+ runtime: RuntimeConfig
133
+ mcps: list[MCPSettings] = Field(default_factory=list)
134
+
135
+ @model_validator(mode="after")
136
+ def validate_mcps(self) -> "AgentDefinition":
137
+ # FIXME: 未来不一定要求必须存在 MCP;当前先保持现有约束。
138
+ if not self.mcps:
139
+ raise ValueError("at least one MCP server must be configured in 'mcps'")
140
+
141
+ names = [mcp.name for mcp in self.mcps]
142
+ if len(set(names)) != len(names):
143
+ raise ValueError("mcp names must be unique")
144
+ return self
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any
5
+
6
+ from .definitions import InputFieldDefinition, ToolPolicy
7
+
8
+
9
+ def parse_user_payload(value: str | dict | None) -> Any:
10
+ if value is None:
11
+ return None
12
+ if isinstance(value, dict):
13
+ return value
14
+ text = str(value).strip()
15
+ if not text:
16
+ return None
17
+ try:
18
+ return json.loads(text)
19
+ except Exception:
20
+ return text
21
+
22
+
23
+ def is_missing(value: Any) -> bool:
24
+ if value is None:
25
+ return True
26
+ if isinstance(value, str):
27
+ return value.strip() == ""
28
+ if isinstance(value, (list, dict)):
29
+ return len(value) == 0
30
+ return False
31
+
32
+
33
+ def collect_missing_fields(policy: ToolPolicy, current_args: dict[str, Any]) -> list[InputFieldDefinition]:
34
+ missing: list[InputFieldDefinition] = []
35
+ for field in policy.input_fields:
36
+ target_arg = field.target_arg
37
+ if field.required and is_missing(current_args.get(target_arg)):
38
+ missing.append(field)
39
+ return missing
40
+
41
+
42
+ def merge_input_fields(
43
+ current_args: dict[str, Any],
44
+ policy: ToolPolicy,
45
+ resume_payload: Any,
46
+ ) -> dict[str, Any]:
47
+ merged = dict(current_args)
48
+ if isinstance(resume_payload, dict):
49
+ for field in policy.input_fields:
50
+ key = field.name
51
+ target_arg = field.target_arg
52
+ if key in resume_payload and not is_missing(resume_payload[key]):
53
+ merged[target_arg] = resume_payload[key]
54
+ elif target_arg in resume_payload and not is_missing(resume_payload[target_arg]):
55
+ merged[target_arg] = resume_payload[target_arg]
56
+ elif field.default is not None and is_missing(merged.get(target_arg)):
57
+ merged[target_arg] = field.default
58
+ return merged
59
+
60
+ if len(policy.input_fields) == 1 and resume_payload is not None:
61
+ field = policy.input_fields[0]
62
+ merged[field.target_arg] = resume_payload
63
+ return merged