55AI decides how to use scripts - users only control whether scripts are allowed.
66"""
77
8+ import json
89import logging
910from typing import Any , Dict , List , Optional
1011
@@ -22,7 +23,10 @@ class ScriptTool(BaseTool):
2223 Tool wrapper for skill scripts.
2324
2425 Exposes a SkillScript as a callable tool that agents can invoke.
25- The AI decides what input to provide - there's no fixed parameter schema.
26+ When the script defines an ``input_schema``, the tool parameters are
27+ derived from that schema so the LLM receives a structured contract.
28+ Otherwise a generic ``input`` string parameter is used for backward
29+ compatibility.
2630 """
2731
2832 name : str = Field (..., description = "Tool name" )
@@ -33,6 +37,7 @@ class ScriptTool(BaseTool):
3337 script : SkillScript = Field (..., exclude = True )
3438 skill_name : str = Field (..., exclude = True )
3539 working_directory : Optional [str ] = Field (default = None , exclude = True )
40+ _uses_structured_schema : bool = False
3641
3742 def __init__ (
3843 self ,
@@ -55,17 +60,49 @@ def __init__(
5560 desc = script .description or f"Execute the '{ script .name } ' script"
5661 description = f"{ desc } (Type: { script .type .value } )"
5762
58- # Simple parameter schema - just optional input
59- parameters = {
60- "type" : "object" ,
61- "properties" : {
62- "input" : {
63- "type" : "string" ,
64- "description" : "Optional input text to pass to the script via stdin"
63+ # Derive parameter schema from script.input_schema when available (#8)
64+ uses_structured = False
65+ if script .input_schema and isinstance (script .input_schema , dict ):
66+ schema_type = script .input_schema .get ("type" , "object" )
67+ # Tool/function calling interfaces expect top-level object schema.
68+ # If skill metadata declares non-object type, degrade gracefully.
69+ if schema_type != "object" :
70+ logger .warning (
71+ "Script '%s' in skill '%s' has non-object input_schema.type=%s; "
72+ "falling back to generic object schema" ,
73+ script .name ,
74+ skill_name ,
75+ schema_type ,
76+ )
77+ parameters = {
78+ "type" : "object" ,
79+ "properties" : {
80+ "input" : {
81+ "type" : "string" ,
82+ "description" : "Optional input text to pass to the script via stdin"
83+ }
84+ },
85+ "required" : []
6586 }
66- },
67- "required" : []
68- }
87+ else :
88+ parameters = {
89+ "type" : "object" ,
90+ "properties" : script .input_schema .get ("properties" , {}),
91+ "required" : script .input_schema .get ("required" , []),
92+ }
93+ uses_structured = True
94+ else :
95+ # Fallback: generic optional input string (backward compat)
96+ parameters = {
97+ "type" : "object" ,
98+ "properties" : {
99+ "input" : {
100+ "type" : "string" ,
101+ "description" : "Optional input text to pass to the script via stdin"
102+ }
103+ },
104+ "required" : []
105+ }
69106
70107 super ().__init__ (
71108 name = tool_name ,
@@ -75,14 +112,20 @@ def __init__(
75112 skill_name = skill_name ,
76113 working_directory = working_directory
77114 )
115+ object .__setattr__ (self , "_uses_structured_schema" , uses_structured )
78116
79117 async def execute (self , input : Optional [str ] = None , ** kwargs ) -> str :
80118 """
81119 Execute the script.
82120
121+ When the script declares an ``input_schema``, the LLM's structured
122+ kwargs are serialized to JSON and piped to stdin. For legacy scripts
123+ that only declare a generic ``input`` string, the raw value is passed
124+ through as-is.
125+
83126 Args:
84- input: Optional input text to pass to script via stdin
85- **kwargs: Additional arguments (ignored)
127+ input: Optional input text (legacy path)
128+ **kwargs: Structured arguments matching input_schema
86129
87130 Returns:
88131 Script output as string
@@ -91,9 +134,24 @@ async def execute(self, input: Optional[str] = None, **kwargs) -> str:
91134
92135 logger .debug (f"ScriptTool '{ self .name } ' executing" )
93136
137+ # Decide what to send to the script on stdin
138+ if self ._uses_structured_schema :
139+ # Build a JSON payload from all kwargs (including 'input' if present)
140+ payload : Dict [str , Any ] = {}
141+ if input is not None :
142+ payload ["input" ] = input
143+ payload .update (kwargs )
144+ input_text = json .dumps (payload , ensure_ascii = False )
145+ else :
146+ # Legacy path: plain string or try JSON passthrough
147+ input_text = input
148+ if input_text is None and kwargs :
149+ # Model may have sent structured args despite generic schema
150+ input_text = json .dumps (kwargs , ensure_ascii = False )
151+
94152 result : ScriptResult = await executor .execute (
95153 script = self .script ,
96- input_text = input ,
154+ input_text = input_text ,
97155 working_directory = self .working_directory
98156 )
99157
0 commit comments