Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions python/packages/core/agent_framework/_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
import time
import zipfile
from abc import ABC, abstractmethod
from collections.abc import Callable, Sequence
from collections.abc import Callable, Mapping, Sequence
from dataclasses import dataclass
from datetime import timedelta
from enum import Enum
Expand All @@ -75,6 +75,7 @@
from pydantic import AnyUrl

from ._agents import SupportsAgentRun
from ._middleware import FunctionInvocationContext
from ._sessions import AgentSession, SessionContext
from ._types import Content

Expand Down Expand Up @@ -2549,13 +2550,52 @@ def _create_tools(
async def _load(skill_name: str) -> str:
return await self._load_skill(skills, skill_name)

async def _read_resource(skill_name: str, resource_name: str, **kwargs: Any) -> Any:
return await self._read_skill_resource(skills, skill_name, resource_name, **kwargs)
# The runtime kwargs these handlers forward come from the host's
# ``agent.run(function_invocation_kwargs=...)``, never from the model. Two
# things keep the channels apart at this seam, and both are required.
#
# First, declaring ``ctx`` opts the handler into FunctionInvocationContext
# injection; without it the function-invocation layer has nowhere to put the
# host values and silently drops them. Second, the handlers take no ``**kwargs``
# and the outer schemas below set ``additionalProperties: False``, so an
# undeclared top-level argument returned by the model is rejected during
# validation instead of binding into the runtime namespace.
#
# ``ctx`` is injected by name and is absent from the advertised schema, so it is
# never something the model can supply or override.
#
# This guarantee covers ``ctx.kwargs``, not everything a skill callback ends up
# seeing. ``args`` stays deliberately free-form because scripts declare their own
# parameters, and :meth:`InlineSkillScript.run` expands it alongside these runtime
# kwargs, so a script's ``**kwargs`` can also hold model-supplied entries that are
# not declared parameters. Resources take no model arguments, so a resource's
# ``**kwargs`` is runtime-only. Do not treat a name appearing in a script's
# ``**kwargs`` as proof the host supplied it.
async def _read_resource(
ctx: FunctionInvocationContext,
skill_name: str,
resource_name: str,
) -> Any:
return await self._read_skill_resource(
skills,
skill_name,
resource_name,
runtime_kwargs=ctx.kwargs,
)

async def _run_script(
skill_name: str, script_name: str, args: dict[str, Any] | list[str] | None = None, **kwargs: Any
ctx: FunctionInvocationContext,
skill_name: str,
script_name: str,
args: dict[str, Any] | list[str] | None = None,
) -> Any:
return await self._run_skill_script(skills, skill_name, script_name, args, **kwargs)
return await self._run_skill_script(
skills,
skill_name,
script_name,
args,
runtime_kwargs=ctx.kwargs,
)

return [
FunctionTool(
Expand All @@ -2569,6 +2609,7 @@ async def _run_script(
"skill_name": {"type": "string", "description": "The name of the skill to load."},
},
"required": ["skill_name"],
"additionalProperties": False,
Comment thread
eavanvalkenburg marked this conversation as resolved.
Resolved
},
),
FunctionTool(
Expand All @@ -2586,6 +2627,7 @@ async def _run_script(
},
},
"required": ["skill_name", "resource_name"],
"additionalProperties": False,
},
),
FunctionTool(
Expand Down Expand Up @@ -2637,6 +2679,9 @@ async def _run_script(
},
},
"required": ["skill_name", "script_name"],
# Scripts declare their own parameters, so the nested ``args`` object
# above stays free-form; only the outer envelope is closed.
"additionalProperties": False,
},
),
]
Expand Down Expand Up @@ -2678,7 +2723,8 @@ async def _run_skill_script(
skill_name: str,
script_name: str,
args: dict[str, Any] | list[str] | None = None,
**kwargs: Any,
*,
runtime_kwargs: Mapping[str, Any] | None = None,
) -> Any:
"""Run a named script from a skill.

Expand All @@ -2691,9 +2737,19 @@ async def _run_skill_script(
script_name: The script name to look up (case-insensitive).
args: Optional arguments for the script, provided by the
agent/LLM.
**kwargs: Runtime keyword arguments forwarded only to script
functions that accept ``**kwargs`` (e.g. arguments passed via
``agent.run(user_id="123")``).
runtime_kwargs: Runtime keyword arguments forwarded only to script
functions that accept ``**kwargs``. This parameter carries host
request context supplied via
``agent.run(..., function_invocation_kwargs={"user_id": "123"})``
and delivered through :class:`FunctionInvocationContext`; it is
never sourced from model-supplied tool arguments. Note that the
receiving script's own ``**kwargs`` is not runtime-only: these
values are expanded alongside any undeclared entries in *args*,
which the model supplies. Taking these as one mapping keeps this
helper's own parameter names (``skills``, ``skill_name``,
``script_name``, ``args``) usable as runtime kwarg names; the
public :meth:`SkillScript.run` signature still expands them, so
``skill`` and ``args`` remain reserved at that boundary.

Returns:
The script result. Returns a user-facing error string for
Expand All @@ -2719,13 +2775,18 @@ async def _run_skill_script(
return f"Error: Script '{script_name}' not found in skill '{skill_name}'."

try:
return await script.run(skill, args, **kwargs)
return await script.run(skill, args, **(runtime_kwargs or {}))
except Exception:
logger.exception("Error running script '%s' in skill '%s'", script_name, skill_name)
raise

async def _read_skill_resource(
self, skills: Sequence[Skill], skill_name: str, resource_name: str, **kwargs: Any
self,
skills: Sequence[Skill],
skill_name: str,
resource_name: str,
*,
runtime_kwargs: Mapping[str, Any] | None = None,
) -> Any:
"""Read a named resource from a skill.

Expand All @@ -2737,9 +2798,12 @@ async def _read_skill_resource(
skills: The skills to look up the skill from.
skill_name: The name of the owning skill.
resource_name: The resource name to look up (case-insensitive).
**kwargs: Runtime keyword arguments forwarded to resource functions
that accept ``**kwargs`` (e.g. arguments passed via
``agent.run(user_id="123")``).
runtime_kwargs: Runtime keyword arguments forwarded to resource functions
that accept ``**kwargs``. These carry host request context
supplied via
``agent.run(..., function_invocation_kwargs={"user_id": "123"})``
and delivered through :class:`FunctionInvocationContext`; they are
never sourced from model-supplied tool arguments.

Returns:
The resource content (any type). Returns a user-facing error
Expand Down Expand Up @@ -2768,7 +2832,7 @@ async def _read_skill_resource(
return f"Error: Resource '{resource_name}' not found in skill '{skill_name}'."

try:
return await resource.read(**kwargs)
return await resource.read(**(runtime_kwargs or {}))
except Exception:
logger.exception("Failed to read resource '%s' from skill '%s'", resource_name, skill_name)
raise
Expand Down
Loading
Loading