agentic-systems 1.0.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 (52) hide show
  1. agentic_systems/__init__.py +92 -0
  2. agentic_systems/agents.py +668 -0
  3. agentic_systems/api.py +219 -0
  4. agentic_systems/bedrock_runtime_client.py +299 -0
  5. agentic_systems/chain.py +108 -0
  6. agentic_systems/cli.py +266 -0
  7. agentic_systems/contracts.py +545 -0
  8. agentic_systems/core/__init__.py +41 -0
  9. agentic_systems/core/agents.py +5 -0
  10. agentic_systems/core/contracts.py +5 -0
  11. agentic_systems/core/human_output.py +5 -0
  12. agentic_systems/core/lineage.py +5 -0
  13. agentic_systems/core/results.py +6 -0
  14. agentic_systems/core/runtime.py +209 -0
  15. agentic_systems/core/scheduler.py +219 -0
  16. agentic_systems/core/tools.py +5 -0
  17. agentic_systems/engines/__init__.py +15 -0
  18. agentic_systems/engines/bedrock.py +69 -0
  19. agentic_systems/engines/names.py +68 -0
  20. agentic_systems/engines/python_direct.py +7 -0
  21. agentic_systems/environments.py +802 -0
  22. agentic_systems/errors.py +19 -0
  23. agentic_systems/evals.py +384 -0
  24. agentic_systems/expectations.py +68 -0
  25. agentic_systems/factories.py +377 -0
  26. agentic_systems/final_answer.py +191 -0
  27. agentic_systems/human_output.py +925 -0
  28. agentic_systems/integrations/__init__.py +10 -0
  29. agentic_systems/integrations/langgraph.py +799 -0
  30. agentic_systems/lineage.py +687 -0
  31. agentic_systems/output_contracts.py +136 -0
  32. agentic_systems/providers/__init__.py +43 -0
  33. agentic_systems/providers/base.py +265 -0
  34. agentic_systems/providers/bedrock_runtime.py +2959 -0
  35. agentic_systems/providers/openai_runtime.py +349 -0
  36. agentic_systems/providers/python_direct.py +327 -0
  37. agentic_systems/providers/vllm_runtime.py +147 -0
  38. agentic_systems/results.py +404 -0
  39. agentic_systems/skills/__init__.py +14 -0
  40. agentic_systems/skills/loader.py +134 -0
  41. agentic_systems/skills/skill.py +230 -0
  42. agentic_systems/system.py +604 -0
  43. agentic_systems/tools/__init__.py +15 -0
  44. agentic_systems/tools/compat.py +159 -0
  45. agentic_systems/tools/decorators.py +105 -0
  46. agentic_systems/tools/tool.py +319 -0
  47. agentic_systems/utils.py +1511 -0
  48. agentic_systems-1.0.0.dist-info/METADATA +335 -0
  49. agentic_systems-1.0.0.dist-info/RECORD +52 -0
  50. agentic_systems-1.0.0.dist-info/WHEEL +5 -0
  51. agentic_systems-1.0.0.dist-info/entry_points.txt +2 -0
  52. agentic_systems-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,668 @@
1
+ """Agent object for Agentic Systems 1.0."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from collections import Counter
8
+ from collections.abc import Callable, Iterable
9
+ from typing import Any
10
+
11
+ from .contracts import AgentContract, RunPolicy, ValidationResult, resolve_policy, validate_contract_policy
12
+ from .core.runtime import RuntimeConfig
13
+ from .core.scheduler import SchedulerConfig, SchedulerTimeoutError, execute_async, execute_sync, merge_policy_with_scheduler
14
+ from .engines.names import (
15
+ BEDROCK_RUNTIME_ENGINE,
16
+ LANGGRAPH_ORCHESTRATOR,
17
+ OPENAI_AGENTS_FRAMEWORK,
18
+ OPENAI_RUNTIME_ENGINE,
19
+ PYTHON_DIRECT_ENGINE,
20
+ STRANDS_FRAMEWORK,
21
+ canonical_engine_name,
22
+ normalize_engine_text,
23
+ )
24
+ from .errors import GraphContractError
25
+ from .results import RunResult
26
+ from .final_answer import OutputSchema, final_answer
27
+ from .skills import LoadedSkill, Skill
28
+ from .tools import Tool
29
+ from .tools.compat import Toolkit, expand_tool_inputs
30
+
31
+
32
+ def _coerce_input(value: Any, input_contract: Any | None) -> Any:
33
+ if input_contract is None:
34
+ return value
35
+ if isinstance(value, input_contract):
36
+ return value
37
+ if isinstance(value, dict):
38
+ return input_contract.model_validate(value)
39
+ return input_contract.model_validate({"input": value})
40
+
41
+
42
+ def _coerce_output_data(result: RunResult, output_contract: Any | None) -> RunResult:
43
+ if output_contract is None:
44
+ if not result.final:
45
+ result.final = final_answer(result.data, text=result.text)
46
+ return result
47
+
48
+ if isinstance(output_contract, OutputSchema):
49
+ result.final = final_answer(result.data or result.final, schema=output_contract, text=result.text)
50
+ return result
51
+
52
+ # Backward-compatible Pydantic output contracts keep validating ``data``.
53
+ # The validated payload also becomes ``final`` because it represents the
54
+ # user-facing structured answer requested by the output contract.
55
+ source = result.data or result.final or _try_parse_json_object(result.text)
56
+ if isinstance(source, dict) and set(source) == {"text"} and source.get("text") == result.text:
57
+ source = _try_parse_json_object(result.text)
58
+ validated = output_contract.model_validate(source)
59
+ payload = validated.model_dump(mode="json")
60
+ result.data = payload
61
+ result.final = payload
62
+ return result
63
+
64
+
65
+ def _try_parse_json_object(text: str) -> dict[str, Any]:
66
+ try:
67
+ parsed = json.loads(text)
68
+ except Exception:
69
+ return {"text": text}
70
+ return parsed if isinstance(parsed, dict) else {"value": parsed}
71
+
72
+
73
+
74
+
75
+ def _resolve_framework_and_engine(engine: str | None, framework: str | None) -> tuple[str, str | None]:
76
+ """Return (execution_engine, user_framework).
77
+
78
+ ``engine`` describes the model/runtime provider, while ``framework``
79
+ describes the orchestration layer.
80
+ """
81
+
82
+ resolved_engine = canonical_engine_name(engine or BEDROCK_RUNTIME_ENGINE)
83
+ if framework is None or str(framework).strip() == "":
84
+ if resolved_engine == LANGGRAPH_ORCHESTRATOR:
85
+ raise ValueError("langgraph_is_not_engine: LangGraph is an orchestrator, not an engine. Use framework='langgraph' with engine='bedrock-runtime'.")
86
+ return resolved_engine, None
87
+
88
+ fw = normalize_engine_text(framework)
89
+ if fw in {LANGGRAPH_ORCHESTRATOR, OPENAI_AGENTS_FRAMEWORK, STRANDS_FRAMEWORK}:
90
+ return resolved_engine, fw
91
+ raise ValueError("framework must be one of: langgraph, openai-agents, strands.")
92
+
93
+ def _framework_label(framework: str | None) -> str:
94
+ return str(framework).strip() if framework not in (None, "", "n/a") else "agentic-systems"
95
+
96
+
97
+ class Agent:
98
+ """Portable agent configuration and execution facade.
99
+
100
+ ``Agent`` can now be created directly as a lightweight public API object:
101
+
102
+ .. code-block:: python
103
+
104
+ agent = Agent(name="calculator", tools=[sumar], skills=[math_skill])
105
+
106
+ A direct agent is a portable configuration. By default it targets the
107
+ canonical cloud engine, ``bedrock-runtime``. Use ``agent.bind(system)`` or
108
+ create the agent through ``system.agent(...)`` to run with AWS credentials.
109
+ For smoke tests without AWS, opt in explicitly with ``engine="python-runtime"``
110
+ and pass a structured tool plan.
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ *,
116
+ name: str,
117
+ instructions: str = "",
118
+ tools: Any = None,
119
+ skills: Any = None,
120
+ system: Any | None = None,
121
+ engine: str | None = None,
122
+ framework: str | None = None,
123
+ model: str | None = None,
124
+ contract: AgentContract | dict[str, Any] | None = None,
125
+ policy: RunPolicy | dict[str, Any] | None = None,
126
+ input_contract: Any | None = None,
127
+ output_contract: Any | None = None,
128
+ input: Any | None = None,
129
+ output: Any | None = None,
130
+ defaults: dict[str, Any] | None = None,
131
+ metadata: dict[str, Any] | None = None,
132
+ runtime: RuntimeConfig | dict[str, Any] | None = None,
133
+ ) -> None:
134
+ self.system = system
135
+ self.name = str(name or "").strip()
136
+ if not self.name:
137
+ raise ValueError("Agent name must be non-empty.")
138
+ self.instructions = str(instructions or "")
139
+ self.runtime_config = RuntimeConfig.coerce(runtime) if runtime is not None else getattr(system, "runtime_config", None)
140
+ if self.runtime_config is not None and engine is None:
141
+ engine = self.runtime_config.provider
142
+ self._engine_was_default = engine is None and framework is None
143
+ self.engine, self.framework = _resolve_framework_and_engine(engine, framework)
144
+ self.model = model if model is not None else (self.runtime_config.model_id if self.runtime_config else getattr(system, "model", None))
145
+ self.contract = AgentContract.coerce(contract)
146
+ self.policy = policy
147
+ self.input_contract = input_contract if input_contract is not None else input
148
+ self.output_contract = output_contract if output_contract is not None else output
149
+ self.defaults = defaults
150
+ self.metadata = dict(metadata or {})
151
+
152
+ explicit_tool_names, explicit_tools = _normalize_agent_tool_inputs(tools)
153
+ skill_tool_names, skill_names, skill_tools, skill_objects = _normalize_agent_skill_inputs(skills)
154
+ self.tools = _dedupe_preserve_order([*skill_tool_names, *explicit_tool_names])
155
+ self.skills = _dedupe_preserve_order(skill_names)
156
+ self._direct_tools = _dedupe_tools([*skill_tools, *explicit_tools])
157
+ self._direct_skills = _dedupe_skills(skill_objects)
158
+
159
+ self.validate().raise_if_failed()
160
+
161
+ @property
162
+ def tool_names(self) -> tuple[str, ...]:
163
+ """Return this agent's resolved tool names."""
164
+
165
+ return self.tools
166
+
167
+ def available_tools(self) -> list[Tool]:
168
+ """Return concrete Tool objects known by this agent.
169
+
170
+ Direct agents can hold concrete tools from ``tools=[...]`` and
171
+ ``skills=[Skill(...)]``. System-backed agents may only know tool names,
172
+ so this method falls back to the system public registry when available.
173
+ """
174
+
175
+ tools = list(self._direct_tools)
176
+ known = {tool.name for tool in tools}
177
+ public_tools = getattr(getattr(self, "system", None), "public_tools", None)
178
+ if public_tools is not None:
179
+ for name in self.tools:
180
+ tool = public_tools.get(name)
181
+ if isinstance(tool, Tool) and tool.name not in known:
182
+ tools.append(tool)
183
+ known.add(tool.name)
184
+ return tools
185
+
186
+ def info(self) -> dict[str, Any]:
187
+ """Return a JSON-like summary of the agent configuration."""
188
+
189
+ runtime_engine = self._runtime_engine_name()
190
+ return {
191
+ "name": self.name,
192
+ "instructions": self.instructions,
193
+ "tools": list(self.tools),
194
+ "skills": list(self.skills),
195
+ "engine": runtime_engine,
196
+ "framework": _framework_label(self.framework),
197
+ "runtime_engine": runtime_engine,
198
+ "execution_engine": self.engine,
199
+ "model": self.model,
200
+ "contract": self.contract.model_dump(mode="json"),
201
+ "policy": _json_like(self.policy),
202
+ "input_contract": _contract_name(self.input_contract),
203
+ "output_contract": _contract_name(self.output_contract),
204
+ "has_system": self.system is not None,
205
+ "direct_tool_count": len(self._direct_tools),
206
+ "direct_tools": [tool.info() for tool in self._direct_tools],
207
+ "metadata": _json_like(self.metadata),
208
+ "runtime": self.runtime_config.to_dict() if self.runtime_config is not None else None,
209
+ }
210
+
211
+ def describe(self) -> str:
212
+ """Return a short human-readable description."""
213
+
214
+ tools = ", ".join(self.tools) if self.tools else "no tools"
215
+ skills = f"; skills: {', '.join(self.skills)}" if self.skills else ""
216
+ return f"Agent `{self.name}` ({tools}{skills})"
217
+
218
+ def check(self) -> ValidationResult:
219
+ """Alias for ``validate`` for symmetry with Tool and Skill."""
220
+
221
+ return self.validate()
222
+
223
+ def bind(self, system: Any) -> "Agent":
224
+ """Attach this direct agent to an ``AgenticSystem``.
225
+
226
+ The method returns the system-backed agent created by ``system.agent``.
227
+ Concrete direct tools are registered first through runtime ``Skill``
228
+ expansion or explicit tool registration paths.
229
+ """
230
+
231
+ if system is None:
232
+ raise TypeError("agent.bind(system) expects an AgenticSystem-like object.")
233
+ if self.system is system:
234
+ return self
235
+
236
+ skills_payload: list[Any] = [*self._direct_skills]
237
+ known_from_skills = {tool.name for skill in self._direct_skills for tool in skill.available_tools()}
238
+ tools_payload: list[Any] = [tool for tool in self._direct_tools if tool.name not in known_from_skills]
239
+ tools_payload.extend(name for name in self.tools if name not in {tool.name for tool in self._direct_tools})
240
+
241
+ return system.agent(
242
+ name=self.name,
243
+ instructions=self.instructions,
244
+ tools=tools_payload,
245
+ skills=skills_payload or list(self.skills),
246
+ engine=self.engine,
247
+ framework=self.framework,
248
+ input=self.input_contract,
249
+ output=self.output_contract,
250
+ contract=self.contract,
251
+ policy=self.policy,
252
+ model=self.model,
253
+ defaults=self.defaults,
254
+ runtime=self.runtime_config,
255
+ )
256
+
257
+ def run(self, input: Any = None, *, mode: str = "default", config: RunPolicy | dict[str, Any] | None = None) -> RunResult:
258
+ """Run the agent from synchronous user code.
259
+
260
+ This is the primary execution method for notebooks, scripts and tests.
261
+ The selected engine is responsible for using its native synchronous
262
+ path, so sync execution does not create a second event loop behind the
263
+ user's back. Use ``agent.arun(...)`` for async applications and async
264
+ LangGraph nodes.
265
+ """
266
+
267
+ clean_input = _coerce_input(input, self.input_contract)
268
+ policy = self._policy_for_runtime(resolve_policy(mode=mode, agent_policy=self.policy, run_config=config))
269
+ scheduler = self._scheduler()
270
+ if self.system is None:
271
+ if self.engine != PYTHON_DIRECT_ENGINE:
272
+ raise RuntimeError(
273
+ f"Direct Agent.run(...) without AgenticSystem cannot use engine={self.engine!r}. "
274
+ "Use `agent.bind(system)` or create the agent through `system.agent(...)` for runtime-backed execution."
275
+ )
276
+ from .providers.python_direct import PythonDirectEngine
277
+
278
+ engine = PythonDirectEngine()
279
+ else:
280
+ engine = self.system._engine(self.engine)
281
+
282
+ def _run_engine() -> RunResult:
283
+ return engine.run(self, clean_input, policy, mode=mode)
284
+
285
+ if scheduler is None:
286
+ result = _run_engine()
287
+ return self._finalize_result(result, clean_input)
288
+
289
+ try:
290
+ result, scheduler_meta = execute_sync(_run_engine, scheduler, is_success=lambda item: bool(getattr(item, "ok", True)))
291
+ except SchedulerTimeoutError as exc:
292
+ result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code="scheduler_timeout")
293
+ scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": True}
294
+ except Exception as exc: # noqa: BLE001 - return scheduler/runtime failures as RunResult.
295
+ result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code=type(exc).__name__)
296
+ scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": False}
297
+ return self._finalize_result(self._attach_scheduler_meta(result, scheduler_meta), clean_input)
298
+
299
+ async def arun(self, input: Any = None, *, mode: str = "default", config: RunPolicy | dict[str, Any] | None = None) -> RunResult:
300
+ """Run the agent from async application code.
301
+
302
+ Async execution uses the engine's native async path when available. If a
303
+ custom sync-only engine is plugged in, Agentic Systems isolates that work
304
+ in a worker thread instead of blocking the caller's event loop.
305
+ """
306
+
307
+ clean_input = _coerce_input(input, self.input_contract)
308
+ policy = self._policy_for_runtime(resolve_policy(mode=mode, agent_policy=self.policy, run_config=config))
309
+ scheduler = self._scheduler()
310
+ if self.system is None:
311
+ if self.engine != PYTHON_DIRECT_ENGINE:
312
+ raise RuntimeError(
313
+ f"Direct Agent.arun(...) without AgenticSystem cannot use engine={self.engine!r}. "
314
+ "Use `agent.bind(system)` or create the agent through `system.agent(...)` for runtime-backed execution."
315
+ )
316
+ from .providers.python_direct import PythonDirectEngine
317
+
318
+ engine = PythonDirectEngine()
319
+ else:
320
+ engine = self.system._engine(self.engine)
321
+
322
+ async def _run_engine() -> RunResult:
323
+ if hasattr(engine, "arun"):
324
+ return await engine.arun(self, clean_input, policy, mode=mode)
325
+ return await asyncio.to_thread(engine.run, self, clean_input, policy, mode=mode)
326
+
327
+ if scheduler is None:
328
+ result = await _run_engine()
329
+ return self._finalize_result(result, clean_input)
330
+
331
+ try:
332
+ result, scheduler_meta = await execute_async(_run_engine, scheduler, is_success=lambda item: bool(getattr(item, "ok", True)))
333
+ except SchedulerTimeoutError as exc:
334
+ result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code="scheduler_timeout")
335
+ scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": True}
336
+ except Exception as exc: # noqa: BLE001 - return scheduler/runtime failures as RunResult.
337
+ result = self._scheduler_failure_result(str(exc), clean_input, mode=mode, code=type(exc).__name__)
338
+ scheduler_meta = {"scheduler": scheduler.to_dict(), "attempts": int(scheduler.max_retries) + 1, "retries": int(scheduler.max_retries), "timed_out": False}
339
+ return self._finalize_result(self._attach_scheduler_meta(result, scheduler_meta), clean_input)
340
+
341
+
342
+ def _scheduler(self) -> SchedulerConfig | None:
343
+ if self.runtime_config is None:
344
+ return None
345
+ return self.runtime_config.scheduler
346
+
347
+ def _policy_for_runtime(self, policy: RunPolicy) -> RunPolicy:
348
+ return merge_policy_with_scheduler(policy, self._scheduler())
349
+
350
+ def _attach_scheduler_meta(self, result: RunResult, scheduler_meta: dict[str, Any]) -> RunResult:
351
+ if self.runtime_config is not None:
352
+ result.meta.setdefault("runtime", self.runtime_config.to_dict())
353
+ result.meta.setdefault("scheduler", self.runtime_config.scheduler.to_dict())
354
+ result.meta["scheduler_execution"] = scheduler_meta
355
+ result.usage.setdefault("scheduler", {})
356
+ result.usage["scheduler"].update(
357
+ {
358
+ "attempts": scheduler_meta.get("attempts"),
359
+ "retries": scheduler_meta.get("retries"),
360
+ "timed_out": scheduler_meta.get("timed_out"),
361
+ "latency_ms": scheduler_meta.get("latency_ms"),
362
+ }
363
+ )
364
+ return result
365
+
366
+ def _scheduler_failure_result(self, message: str, clean_input: Any, *, mode: str, code: str) -> RunResult:
367
+ runtime_engine = self._runtime_engine_name()
368
+ return RunResult(
369
+ text=message,
370
+ data={"ok": False, "error": {"code": code, "message": message}},
371
+ ok=False,
372
+ engine=runtime_engine,
373
+ model=self.model or "",
374
+ mode=mode,
375
+ meta={
376
+ "input": _json_like(clean_input),
377
+ "source_result_type": "SchedulerFailure",
378
+ "framework": _framework_label(self.framework),
379
+ "runtime_engine": runtime_engine,
380
+ "execution_engine": self.engine,
381
+ },
382
+ )
383
+
384
+ def _runtime_engine_name(self) -> str:
385
+ return self.engine
386
+
387
+ def _finalize_result(self, result: RunResult, clean_input: Any | None = None) -> RunResult:
388
+ result.meta["framework"] = _framework_label(self.framework)
389
+ if clean_input is not None:
390
+ result.meta.setdefault("input", _json_like(clean_input))
391
+ runtime_engine = self._runtime_engine_name()
392
+ result.meta.setdefault("runtime_engine", runtime_engine)
393
+ result.meta.setdefault("execution_engine", self.engine)
394
+ if result.engine == self.engine:
395
+ result.engine = runtime_engine
396
+ result = _coerce_output_data(result, self.output_contract)
397
+ validation = result.validate(self.contract)
398
+ result.validation = validation.to_dict()
399
+ result.ok = result.ok and validation.ok
400
+ return result
401
+
402
+ def run_sync(self, input: Any = None, *, mode: str = "default", config: RunPolicy | dict[str, Any] | None = None) -> RunResult:
403
+ """Alias for ``run`` for call sites that want explicit sync naming."""
404
+
405
+ return self.run(input, mode=mode, config=config)
406
+
407
+ def as_node(
408
+ self,
409
+ *,
410
+ input: str | Callable[[dict[str, Any]], Any] = "prompt",
411
+ output: str | Callable[[RunResult, dict[str, Any]], Any] | None = "answer",
412
+ trace: str | None = "ada_trace",
413
+ mode: str = "default",
414
+ config: RunPolicy | dict[str, Any] | None = None,
415
+ ):
416
+ """Return a sync LangGraph-compatible node with partial state updates."""
417
+
418
+ def _node(state: dict[str, Any]) -> Any:
419
+ prompt = _read_node_input(state, input)
420
+ result = self.run(prompt, mode=mode, config=config)
421
+ return _map_node_output(result, state, output, trace)
422
+
423
+ return _node
424
+
425
+ def as_async_node(
426
+ self,
427
+ *,
428
+ input: str | Callable[[dict[str, Any]], Any] = "prompt",
429
+ output: str | Callable[[RunResult, dict[str, Any]], Any] | None = "answer",
430
+ trace: str | None = "ada_trace",
431
+ mode: str = "default",
432
+ config: RunPolicy | dict[str, Any] | None = None,
433
+ ):
434
+ """Return an async LangGraph-compatible node.
435
+
436
+ Use this node with LangGraph ``ainvoke``/async graphs when the engine or
437
+ surrounding application is async-first.
438
+ """
439
+
440
+ async def _node(state: dict[str, Any]) -> Any:
441
+ prompt = _read_node_input(state, input)
442
+ result = await self.arun(prompt, mode=mode, config=config)
443
+ return _map_node_output(result, state, output, trace)
444
+
445
+ return _node
446
+
447
+ def as_tool(self, *, name: str | None = None, description: str | None = None):
448
+ tool_name = name or self.name
449
+
450
+ def _agent_tool(prompt: str) -> dict[str, Any]:
451
+ """Run this agent as a dict-returning tool."""
452
+
453
+ result = self.run(prompt)
454
+ return {"text": result.text, "data": result.data, "ok": result.ok, "trace": result.trace("compact")}
455
+
456
+ _agent_tool.__name__ = tool_name.replace(".", "_")
457
+ _agent_tool.__doc__ = description or f"Run agent {self.name}."
458
+ return _agent_tool
459
+
460
+ def validate(self) -> ValidationResult:
461
+ result = ValidationResult(ok=True)
462
+ if self.engine == LANGGRAPH_ORCHESTRATOR:
463
+ result.add(
464
+ "langgraph_is_not_engine",
465
+ "LangGraph is an orchestrator, not an AgenticSystem engine. Use agent.as_node(...) or agent.as_async_node(...).",
466
+ path="agent.engine",
467
+ )
468
+
469
+ counts = Counter(self.tools)
470
+ for tool_name in sorted(name for name, count in counts.items() if count > 1):
471
+ result.add(
472
+ "duplicate_agent_tool",
473
+ f"Agent '{self.name}' references duplicate tool '{tool_name}'.",
474
+ path="agent.tools",
475
+ meta={"tool_name": tool_name, "count": counts[tool_name]},
476
+ )
477
+
478
+ for index, tool in enumerate(self._direct_tools):
479
+ tool_validation = tool.check()
480
+ for issue in tool_validation.issues:
481
+ result.add(
482
+ issue.code,
483
+ issue.message,
484
+ severity=issue.severity,
485
+ path=f"agent.tools[{index}].{issue.path}" if issue.path else f"agent.tools[{index}]",
486
+ meta={"tool_name": tool.name, **issue.meta},
487
+ )
488
+
489
+ available = set(self.tools)
490
+ if self.system is not None:
491
+ system_tools = set(self.system.tool_names)
492
+ available |= system_tools
493
+ for name in self.tools:
494
+ if name not in system_tools and name not in {tool.name for tool in self._direct_tools}:
495
+ result.add(
496
+ "unknown_agent_tool",
497
+ f"Agent '{self.name}' references unknown tool '{name}'.",
498
+ path="agent.tools",
499
+ meta={"available_tools": sorted(system_tools)},
500
+ )
501
+
502
+ for name in self.contract.must_call + self.contract.must_not_call:
503
+ if name not in available:
504
+ result.add(
505
+ "contract_references_unknown_tool",
506
+ f"Contract for agent '{self.name}' references unknown tool '{name}'.",
507
+ path="agent.contract",
508
+ meta={"available_tools": sorted(available)},
509
+ )
510
+
511
+ static_validation = validate_contract_policy(
512
+ self.contract,
513
+ self.policy,
514
+ available_tools=available,
515
+ )
516
+ for issue in static_validation.issues:
517
+ if issue.code == "contract_references_unknown_tool":
518
+ # Keep the historical, agent-specific error above for backwards compatibility.
519
+ continue
520
+ result.add(
521
+ issue.code,
522
+ issue.message,
523
+ severity=issue.severity,
524
+ path=f"agent.{issue.path}" if issue.path else "agent.contract_policy",
525
+ meta=issue.meta,
526
+ )
527
+ return result
528
+
529
+ def eval(self, cases: list[dict[str, Any]], **kwargs: Any):
530
+ if self.system is None:
531
+ raise RuntimeError("Direct Agent.eval(...) needs an attached AgenticSystem. Use `agent.bind(system)` first.")
532
+ return self.system.eval(self, cases, **kwargs)
533
+
534
+
535
+ def _normalize_agent_tool_inputs(items: Any) -> tuple[tuple[str, ...], tuple[Tool, ...]]:
536
+ """Normalize direct agent tool inputs into names and concrete Tool objects."""
537
+
538
+ if items is None:
539
+ return (), ()
540
+ if isinstance(items, Tool):
541
+ return (items.name,), (items,)
542
+ if isinstance(items, Toolkit):
543
+ return items.tool_names, ()
544
+ if isinstance(items, str):
545
+ return (items,), ()
546
+ if callable(items):
547
+ tool = Tool(name=getattr(items, "__name__", ""), function=items)
548
+ return (tool.name,), (tool,)
549
+ if isinstance(items, Iterable) and not isinstance(items, (str, bytes, dict)):
550
+ names: list[str] = []
551
+ tools: list[Tool] = []
552
+ for item in items:
553
+ item_names, item_tools = _normalize_agent_tool_inputs(item)
554
+ names.extend(item_names)
555
+ tools.extend(item_tools)
556
+ return tuple(names), tuple(tools)
557
+ try:
558
+ return expand_tool_inputs(items), ()
559
+ except TypeError as exc:
560
+ raise TypeError(f"Unsupported agent tools value: {items!r}") from exc
561
+
562
+
563
+ def _normalize_agent_skill_inputs(items: Any) -> tuple[tuple[str, ...], tuple[str, ...], tuple[Tool, ...], tuple[Skill, ...]]:
564
+ """Normalize runtime/loaded skills for direct Agent construction."""
565
+
566
+ if items is None:
567
+ return (), (), (), ()
568
+ if isinstance(items, Skill):
569
+ return items.tool_names, (items.name,), tuple(items.available_tools()), (items,)
570
+ if isinstance(items, LoadedSkill):
571
+ runtime_skill = getattr(items, "runtime_skill", None)
572
+ if isinstance(runtime_skill, Skill):
573
+ return runtime_skill.tool_names, (runtime_skill.name,), tuple(runtime_skill.available_tools()), (runtime_skill,)
574
+ return tuple(items.manifest.tools), (items.manifest.name,), (), ()
575
+ if isinstance(items, str):
576
+ return (), (items,), (), ()
577
+ if isinstance(items, Iterable) and not isinstance(items, (str, bytes, dict)):
578
+ tool_names: list[str] = []
579
+ skill_names: list[str] = []
580
+ tools: list[Tool] = []
581
+ skills: list[Skill] = []
582
+ for item in items:
583
+ item_tool_names, item_skill_names, item_tools, item_skills = _normalize_agent_skill_inputs(item)
584
+ tool_names.extend(item_tool_names)
585
+ skill_names.extend(item_skill_names)
586
+ tools.extend(item_tools)
587
+ skills.extend(item_skills)
588
+ return tuple(tool_names), tuple(skill_names), tuple(tools), tuple(skills)
589
+ raise TypeError(f"Unsupported agent skills value: {items!r}")
590
+
591
+
592
+ def _dedupe_preserve_order(values: Any) -> tuple[str, ...]:
593
+ out: list[str] = []
594
+ seen: set[str] = set()
595
+ for value in values or []:
596
+ text = str(value)
597
+ if text not in seen:
598
+ out.append(text)
599
+ seen.add(text)
600
+ return tuple(out)
601
+
602
+
603
+ def _dedupe_tools(tools: Iterable[Tool]) -> tuple[Tool, ...]:
604
+ out: list[Tool] = []
605
+ seen: set[str] = set()
606
+ for tool in tools:
607
+ if tool.name not in seen:
608
+ out.append(tool)
609
+ seen.add(tool.name)
610
+ return tuple(out)
611
+
612
+
613
+ def _dedupe_skills(skills: Iterable[Skill]) -> tuple[Skill, ...]:
614
+ out: list[Skill] = []
615
+ seen: set[str] = set()
616
+ for skill in skills:
617
+ if skill.name not in seen:
618
+ out.append(skill)
619
+ seen.add(skill.name)
620
+ return tuple(out)
621
+
622
+
623
+ def _json_like(value: Any) -> Any:
624
+ if value is None:
625
+ return None
626
+ if hasattr(value, "model_dump"):
627
+ return value.model_dump(mode="json")
628
+ if isinstance(value, dict):
629
+ return {str(key): _json_like(item) for key, item in value.items()}
630
+ if isinstance(value, (list, tuple, set, frozenset)):
631
+ return [_json_like(item) for item in value]
632
+ if isinstance(value, type):
633
+ return value.__name__
634
+ return value
635
+
636
+
637
+ def _contract_name(value: Any) -> str | None:
638
+ if value is None:
639
+ return None
640
+ return getattr(value, "__name__", type(value).__name__)
641
+
642
+
643
+ def _read_node_input(state: dict[str, Any], input: str | Callable[[dict[str, Any]], Any]) -> Any:
644
+ if callable(input):
645
+ return input(state)
646
+ if input not in state:
647
+ raise GraphContractError(
648
+ f"input key '{input}' not found in state. Available keys: {sorted(state.keys())}. "
649
+ "Fix: pass input='<existing_key>' or input=lambda state: ..."
650
+ )
651
+ return state[input]
652
+
653
+
654
+ def _map_node_output(
655
+ result: RunResult,
656
+ state: dict[str, Any],
657
+ output: str | Callable[[RunResult, dict[str, Any]], Any] | None,
658
+ trace: str | None,
659
+ ) -> Any:
660
+ if callable(output):
661
+ return output(result, state)
662
+
663
+ update: dict[str, Any] = {}
664
+ if output is not None:
665
+ update[output] = result.text
666
+ if trace is not None:
667
+ update[trace] = result.trace("compact")
668
+ return update