kiln_ai.adapters.model_adapters.base_adapter
1import json 2from abc import ABCMeta, abstractmethod 3from dataclasses import dataclass 4from typing import Dict, Literal, Tuple 5 6import jsonschema 7 8from kiln_ai.adapters.ml_model_list import KilnModelProvider, StructuredOutputMode 9from kiln_ai.adapters.parsers.json_parser import parse_json_string 10from kiln_ai.adapters.parsers.parser_registry import model_parser_from_id 11from kiln_ai.adapters.parsers.request_formatters import request_formatter_from_id 12from kiln_ai.adapters.prompt_builders import prompt_builder_from_id 13from kiln_ai.adapters.provider_tools import kiln_model_provider_from 14from kiln_ai.adapters.run_output import RunOutput 15from kiln_ai.datamodel import ( 16 DataSource, 17 DataSourceType, 18 Task, 19 TaskOutput, 20 TaskRun, 21 Usage, 22) 23from kiln_ai.datamodel.json_schema import validate_schema_with_value_error 24from kiln_ai.datamodel.task import RunConfig 25from kiln_ai.utils.config import Config 26 27 28@dataclass 29class AdapterConfig: 30 """ 31 An adapter config is config options that do NOT impact the output of the model. 32 33 For example: if it's saved, of if we request additional data like logprobs. 34 """ 35 36 allow_saving: bool = True 37 top_logprobs: int | None = None 38 default_tags: list[str] | None = None 39 40 41COT_FINAL_ANSWER_PROMPT = "Considering the above, return a final result." 42 43 44class BaseAdapter(metaclass=ABCMeta): 45 """Base class for AI model adapters that handle task execution. 46 47 This abstract class provides the foundation for implementing model-specific adapters 48 that can process tasks with structured or unstructured inputs/outputs. It handles 49 input/output validation, prompt building, and run tracking. 50 51 Attributes: 52 prompt_builder (BasePromptBuilder): Builder for constructing prompts for the model 53 kiln_task (Task): The task configuration and metadata 54 output_schema (dict | None): JSON schema for validating structured outputs 55 input_schema (dict | None): JSON schema for validating structured inputs 56 """ 57 58 def __init__( 59 self, 60 run_config: RunConfig, 61 config: AdapterConfig | None = None, 62 ): 63 self.run_config = run_config 64 self.prompt_builder = prompt_builder_from_id( 65 run_config.prompt_id, run_config.task 66 ) 67 self._model_provider: KilnModelProvider | None = None 68 69 self.output_schema = self.task().output_json_schema 70 self.input_schema = self.task().input_json_schema 71 self.base_adapter_config = config or AdapterConfig() 72 73 def task(self) -> Task: 74 return self.run_config.task 75 76 def model_provider(self) -> KilnModelProvider: 77 """ 78 Lazy load the model provider for this adapter. 79 """ 80 if self._model_provider is not None: 81 return self._model_provider 82 if not self.run_config.model_name or not self.run_config.model_provider_name: 83 raise ValueError("model_name and model_provider_name must be provided") 84 self._model_provider = kiln_model_provider_from( 85 self.run_config.model_name, self.run_config.model_provider_name 86 ) 87 if not self._model_provider: 88 raise ValueError( 89 f"model_provider_name {self.run_config.model_provider_name} not found for model {self.run_config.model_name}" 90 ) 91 return self._model_provider 92 93 async def invoke( 94 self, 95 input: Dict | str, 96 input_source: DataSource | None = None, 97 ) -> TaskRun: 98 run_output, _ = await self.invoke_returning_run_output(input, input_source) 99 return run_output 100 101 async def invoke_returning_run_output( 102 self, 103 input: Dict | str, 104 input_source: DataSource | None = None, 105 ) -> Tuple[TaskRun, RunOutput]: 106 # validate input 107 if self.input_schema is not None: 108 if not isinstance(input, dict): 109 raise ValueError(f"structured input is not a dict: {input}") 110 111 validate_schema_with_value_error( 112 input, 113 self.input_schema, 114 "This task requires a specific input schema. While the model produced JSON, that JSON didn't meet the schema. Search 'Troubleshooting Structured Data Issues' in our docs for more information.", 115 ) 116 117 # Format model input for model call (we save the original input in the task without formatting) 118 formatted_input = input 119 formatter_id = self.model_provider().formatter 120 if formatter_id is not None: 121 formatter = request_formatter_from_id(formatter_id) 122 formatted_input = formatter.format_input(input) 123 124 # Run 125 run_output, usage = await self._run(formatted_input) 126 127 # Parse 128 provider = self.model_provider() 129 parser = model_parser_from_id(provider.parser) 130 parsed_output = parser.parse_output(original_output=run_output) 131 132 # validate output 133 if self.output_schema is not None: 134 # Parse json to dict if we have structured output 135 if isinstance(parsed_output.output, str): 136 parsed_output.output = parse_json_string(parsed_output.output) 137 138 if not isinstance(parsed_output.output, dict): 139 raise RuntimeError( 140 f"structured response is not a dict: {parsed_output.output}" 141 ) 142 validate_schema_with_value_error( 143 parsed_output.output, 144 self.output_schema, 145 "This task requires a specific output schema. While the model produced JSON, that JSON didn't meet the schema. Search 'Troubleshooting Structured Data Issues' in our docs for more information.", 146 ) 147 else: 148 if not isinstance(parsed_output.output, str): 149 raise RuntimeError( 150 f"response is not a string for non-structured task: {parsed_output.output}" 151 ) 152 153 # Validate reasoning content is present (if reasoning) 154 if provider.reasoning_capable and ( 155 not parsed_output.intermediate_outputs 156 or "reasoning" not in parsed_output.intermediate_outputs 157 ): 158 raise RuntimeError( 159 "Reasoning is required for this model, but no reasoning was returned." 160 ) 161 162 # Generate the run and output 163 run = self.generate_run(input, input_source, parsed_output, usage) 164 165 # Save the run if configured to do so, and we have a path to save to 166 if ( 167 self.base_adapter_config.allow_saving 168 and Config.shared().autosave_runs 169 and self.task().path is not None 170 ): 171 run.save_to_file() 172 else: 173 # Clear the ID to indicate it's not persisted 174 run.id = None 175 176 return run, run_output 177 178 def has_structured_output(self) -> bool: 179 return self.output_schema is not None 180 181 @abstractmethod 182 def adapter_name(self) -> str: 183 pass 184 185 @abstractmethod 186 async def _run(self, input: Dict | str) -> Tuple[RunOutput, Usage | None]: 187 pass 188 189 def build_prompt(self) -> str: 190 # The prompt builder needs to know if we want to inject formatting instructions 191 provider = self.model_provider() 192 add_json_instructions = self.has_structured_output() and ( 193 provider.structured_output_mode == StructuredOutputMode.json_instructions 194 or provider.structured_output_mode 195 == StructuredOutputMode.json_instruction_and_object 196 ) 197 198 return self.prompt_builder.build_prompt( 199 include_json_instructions=add_json_instructions 200 ) 201 202 def run_strategy( 203 self, 204 ) -> Tuple[Literal["cot_as_message", "cot_two_call", "basic"], str | None]: 205 # Determine the run strategy for COT prompting. 3 options: 206 # 1. "Thinking" LLM designed to output thinking in a structured format plus a COT prompt: we make 1 call to the LLM, which outputs thinking in a structured format. We include the thinking instuctions as a message. 207 # 2. Normal LLM with COT prompt: we make 2 calls to the LLM - one for thinking and one for the final response. This helps us use the LLM's structured output modes (json_schema, tools, etc), which can't be used in a single call. It also separates the thinking from the final response. 208 # 3. Non chain of thought: we make 1 call to the LLM, with no COT prompt. 209 cot_prompt = self.prompt_builder.chain_of_thought_prompt() 210 reasoning_capable = self.model_provider().reasoning_capable 211 212 if cot_prompt and reasoning_capable: 213 # 1: "Thinking" LLM designed to output thinking in a structured format 214 # A simple message with the COT prompt appended to the message list is sufficient 215 return "cot_as_message", cot_prompt 216 elif cot_prompt: 217 # 2: Unstructured output with COT 218 # Two calls to separate the thinking from the final response 219 return "cot_two_call", cot_prompt 220 else: 221 return "basic", None 222 223 # create a run and task output 224 def generate_run( 225 self, 226 input: Dict | str, 227 input_source: DataSource | None, 228 run_output: RunOutput, 229 usage: Usage | None = None, 230 ) -> TaskRun: 231 # Convert input and output to JSON strings if they are dictionaries 232 input_str = ( 233 json.dumps(input, ensure_ascii=False) if isinstance(input, dict) else input 234 ) 235 output_str = ( 236 json.dumps(run_output.output, ensure_ascii=False) 237 if isinstance(run_output.output, dict) 238 else run_output.output 239 ) 240 241 # If no input source is provided, use the human data source 242 if input_source is None: 243 input_source = DataSource( 244 type=DataSourceType.human, 245 properties={"created_by": Config.shared().user_id}, 246 ) 247 248 new_task_run = TaskRun( 249 parent=self.task(), 250 input=input_str, 251 input_source=input_source, 252 output=TaskOutput( 253 output=output_str, 254 # Synthetic since an adapter, not a human, is creating this 255 source=DataSource( 256 type=DataSourceType.synthetic, 257 properties=self._properties_for_task_output(), 258 ), 259 ), 260 intermediate_outputs=run_output.intermediate_outputs, 261 tags=self.base_adapter_config.default_tags or [], 262 usage=usage, 263 ) 264 265 return new_task_run 266 267 def _properties_for_task_output(self) -> Dict[str, str | int | float]: 268 props = {} 269 270 # adapter info 271 props["adapter_name"] = self.adapter_name() 272 props["model_name"] = self.run_config.model_name 273 props["model_provider"] = self.run_config.model_provider_name 274 props["prompt_id"] = self.run_config.prompt_id 275 276 return props
29@dataclass 30class AdapterConfig: 31 """ 32 An adapter config is config options that do NOT impact the output of the model. 33 34 For example: if it's saved, of if we request additional data like logprobs. 35 """ 36 37 allow_saving: bool = True 38 top_logprobs: int | None = None 39 default_tags: list[str] | None = None
An adapter config is config options that do NOT impact the output of the model.
For example: if it's saved, of if we request additional data like logprobs.
45class BaseAdapter(metaclass=ABCMeta): 46 """Base class for AI model adapters that handle task execution. 47 48 This abstract class provides the foundation for implementing model-specific adapters 49 that can process tasks with structured or unstructured inputs/outputs. It handles 50 input/output validation, prompt building, and run tracking. 51 52 Attributes: 53 prompt_builder (BasePromptBuilder): Builder for constructing prompts for the model 54 kiln_task (Task): The task configuration and metadata 55 output_schema (dict | None): JSON schema for validating structured outputs 56 input_schema (dict | None): JSON schema for validating structured inputs 57 """ 58 59 def __init__( 60 self, 61 run_config: RunConfig, 62 config: AdapterConfig | None = None, 63 ): 64 self.run_config = run_config 65 self.prompt_builder = prompt_builder_from_id( 66 run_config.prompt_id, run_config.task 67 ) 68 self._model_provider: KilnModelProvider | None = None 69 70 self.output_schema = self.task().output_json_schema 71 self.input_schema = self.task().input_json_schema 72 self.base_adapter_config = config or AdapterConfig() 73 74 def task(self) -> Task: 75 return self.run_config.task 76 77 def model_provider(self) -> KilnModelProvider: 78 """ 79 Lazy load the model provider for this adapter. 80 """ 81 if self._model_provider is not None: 82 return self._model_provider 83 if not self.run_config.model_name or not self.run_config.model_provider_name: 84 raise ValueError("model_name and model_provider_name must be provided") 85 self._model_provider = kiln_model_provider_from( 86 self.run_config.model_name, self.run_config.model_provider_name 87 ) 88 if not self._model_provider: 89 raise ValueError( 90 f"model_provider_name {self.run_config.model_provider_name} not found for model {self.run_config.model_name}" 91 ) 92 return self._model_provider 93 94 async def invoke( 95 self, 96 input: Dict | str, 97 input_source: DataSource | None = None, 98 ) -> TaskRun: 99 run_output, _ = await self.invoke_returning_run_output(input, input_source) 100 return run_output 101 102 async def invoke_returning_run_output( 103 self, 104 input: Dict | str, 105 input_source: DataSource | None = None, 106 ) -> Tuple[TaskRun, RunOutput]: 107 # validate input 108 if self.input_schema is not None: 109 if not isinstance(input, dict): 110 raise ValueError(f"structured input is not a dict: {input}") 111 112 validate_schema_with_value_error( 113 input, 114 self.input_schema, 115 "This task requires a specific input schema. While the model produced JSON, that JSON didn't meet the schema. Search 'Troubleshooting Structured Data Issues' in our docs for more information.", 116 ) 117 118 # Format model input for model call (we save the original input in the task without formatting) 119 formatted_input = input 120 formatter_id = self.model_provider().formatter 121 if formatter_id is not None: 122 formatter = request_formatter_from_id(formatter_id) 123 formatted_input = formatter.format_input(input) 124 125 # Run 126 run_output, usage = await self._run(formatted_input) 127 128 # Parse 129 provider = self.model_provider() 130 parser = model_parser_from_id(provider.parser) 131 parsed_output = parser.parse_output(original_output=run_output) 132 133 # validate output 134 if self.output_schema is not None: 135 # Parse json to dict if we have structured output 136 if isinstance(parsed_output.output, str): 137 parsed_output.output = parse_json_string(parsed_output.output) 138 139 if not isinstance(parsed_output.output, dict): 140 raise RuntimeError( 141 f"structured response is not a dict: {parsed_output.output}" 142 ) 143 validate_schema_with_value_error( 144 parsed_output.output, 145 self.output_schema, 146 "This task requires a specific output schema. While the model produced JSON, that JSON didn't meet the schema. Search 'Troubleshooting Structured Data Issues' in our docs for more information.", 147 ) 148 else: 149 if not isinstance(parsed_output.output, str): 150 raise RuntimeError( 151 f"response is not a string for non-structured task: {parsed_output.output}" 152 ) 153 154 # Validate reasoning content is present (if reasoning) 155 if provider.reasoning_capable and ( 156 not parsed_output.intermediate_outputs 157 or "reasoning" not in parsed_output.intermediate_outputs 158 ): 159 raise RuntimeError( 160 "Reasoning is required for this model, but no reasoning was returned." 161 ) 162 163 # Generate the run and output 164 run = self.generate_run(input, input_source, parsed_output, usage) 165 166 # Save the run if configured to do so, and we have a path to save to 167 if ( 168 self.base_adapter_config.allow_saving 169 and Config.shared().autosave_runs 170 and self.task().path is not None 171 ): 172 run.save_to_file() 173 else: 174 # Clear the ID to indicate it's not persisted 175 run.id = None 176 177 return run, run_output 178 179 def has_structured_output(self) -> bool: 180 return self.output_schema is not None 181 182 @abstractmethod 183 def adapter_name(self) -> str: 184 pass 185 186 @abstractmethod 187 async def _run(self, input: Dict | str) -> Tuple[RunOutput, Usage | None]: 188 pass 189 190 def build_prompt(self) -> str: 191 # The prompt builder needs to know if we want to inject formatting instructions 192 provider = self.model_provider() 193 add_json_instructions = self.has_structured_output() and ( 194 provider.structured_output_mode == StructuredOutputMode.json_instructions 195 or provider.structured_output_mode 196 == StructuredOutputMode.json_instruction_and_object 197 ) 198 199 return self.prompt_builder.build_prompt( 200 include_json_instructions=add_json_instructions 201 ) 202 203 def run_strategy( 204 self, 205 ) -> Tuple[Literal["cot_as_message", "cot_two_call", "basic"], str | None]: 206 # Determine the run strategy for COT prompting. 3 options: 207 # 1. "Thinking" LLM designed to output thinking in a structured format plus a COT prompt: we make 1 call to the LLM, which outputs thinking in a structured format. We include the thinking instuctions as a message. 208 # 2. Normal LLM with COT prompt: we make 2 calls to the LLM - one for thinking and one for the final response. This helps us use the LLM's structured output modes (json_schema, tools, etc), which can't be used in a single call. It also separates the thinking from the final response. 209 # 3. Non chain of thought: we make 1 call to the LLM, with no COT prompt. 210 cot_prompt = self.prompt_builder.chain_of_thought_prompt() 211 reasoning_capable = self.model_provider().reasoning_capable 212 213 if cot_prompt and reasoning_capable: 214 # 1: "Thinking" LLM designed to output thinking in a structured format 215 # A simple message with the COT prompt appended to the message list is sufficient 216 return "cot_as_message", cot_prompt 217 elif cot_prompt: 218 # 2: Unstructured output with COT 219 # Two calls to separate the thinking from the final response 220 return "cot_two_call", cot_prompt 221 else: 222 return "basic", None 223 224 # create a run and task output 225 def generate_run( 226 self, 227 input: Dict | str, 228 input_source: DataSource | None, 229 run_output: RunOutput, 230 usage: Usage | None = None, 231 ) -> TaskRun: 232 # Convert input and output to JSON strings if they are dictionaries 233 input_str = ( 234 json.dumps(input, ensure_ascii=False) if isinstance(input, dict) else input 235 ) 236 output_str = ( 237 json.dumps(run_output.output, ensure_ascii=False) 238 if isinstance(run_output.output, dict) 239 else run_output.output 240 ) 241 242 # If no input source is provided, use the human data source 243 if input_source is None: 244 input_source = DataSource( 245 type=DataSourceType.human, 246 properties={"created_by": Config.shared().user_id}, 247 ) 248 249 new_task_run = TaskRun( 250 parent=self.task(), 251 input=input_str, 252 input_source=input_source, 253 output=TaskOutput( 254 output=output_str, 255 # Synthetic since an adapter, not a human, is creating this 256 source=DataSource( 257 type=DataSourceType.synthetic, 258 properties=self._properties_for_task_output(), 259 ), 260 ), 261 intermediate_outputs=run_output.intermediate_outputs, 262 tags=self.base_adapter_config.default_tags or [], 263 usage=usage, 264 ) 265 266 return new_task_run 267 268 def _properties_for_task_output(self) -> Dict[str, str | int | float]: 269 props = {} 270 271 # adapter info 272 props["adapter_name"] = self.adapter_name() 273 props["model_name"] = self.run_config.model_name 274 props["model_provider"] = self.run_config.model_provider_name 275 props["prompt_id"] = self.run_config.prompt_id 276 277 return props
Base class for AI model adapters that handle task execution.
This abstract class provides the foundation for implementing model-specific adapters that can process tasks with structured or unstructured inputs/outputs. It handles input/output validation, prompt building, and run tracking.
Attributes: prompt_builder (BasePromptBuilder): Builder for constructing prompts for the model kiln_task (Task): The task configuration and metadata output_schema (dict | None): JSON schema for validating structured outputs input_schema (dict | None): JSON schema for validating structured inputs
77 def model_provider(self) -> KilnModelProvider: 78 """ 79 Lazy load the model provider for this adapter. 80 """ 81 if self._model_provider is not None: 82 return self._model_provider 83 if not self.run_config.model_name or not self.run_config.model_provider_name: 84 raise ValueError("model_name and model_provider_name must be provided") 85 self._model_provider = kiln_model_provider_from( 86 self.run_config.model_name, self.run_config.model_provider_name 87 ) 88 if not self._model_provider: 89 raise ValueError( 90 f"model_provider_name {self.run_config.model_provider_name} not found for model {self.run_config.model_name}" 91 ) 92 return self._model_provider
Lazy load the model provider for this adapter.
102 async def invoke_returning_run_output( 103 self, 104 input: Dict | str, 105 input_source: DataSource | None = None, 106 ) -> Tuple[TaskRun, RunOutput]: 107 # validate input 108 if self.input_schema is not None: 109 if not isinstance(input, dict): 110 raise ValueError(f"structured input is not a dict: {input}") 111 112 validate_schema_with_value_error( 113 input, 114 self.input_schema, 115 "This task requires a specific input schema. While the model produced JSON, that JSON didn't meet the schema. Search 'Troubleshooting Structured Data Issues' in our docs for more information.", 116 ) 117 118 # Format model input for model call (we save the original input in the task without formatting) 119 formatted_input = input 120 formatter_id = self.model_provider().formatter 121 if formatter_id is not None: 122 formatter = request_formatter_from_id(formatter_id) 123 formatted_input = formatter.format_input(input) 124 125 # Run 126 run_output, usage = await self._run(formatted_input) 127 128 # Parse 129 provider = self.model_provider() 130 parser = model_parser_from_id(provider.parser) 131 parsed_output = parser.parse_output(original_output=run_output) 132 133 # validate output 134 if self.output_schema is not None: 135 # Parse json to dict if we have structured output 136 if isinstance(parsed_output.output, str): 137 parsed_output.output = parse_json_string(parsed_output.output) 138 139 if not isinstance(parsed_output.output, dict): 140 raise RuntimeError( 141 f"structured response is not a dict: {parsed_output.output}" 142 ) 143 validate_schema_with_value_error( 144 parsed_output.output, 145 self.output_schema, 146 "This task requires a specific output schema. While the model produced JSON, that JSON didn't meet the schema. Search 'Troubleshooting Structured Data Issues' in our docs for more information.", 147 ) 148 else: 149 if not isinstance(parsed_output.output, str): 150 raise RuntimeError( 151 f"response is not a string for non-structured task: {parsed_output.output}" 152 ) 153 154 # Validate reasoning content is present (if reasoning) 155 if provider.reasoning_capable and ( 156 not parsed_output.intermediate_outputs 157 or "reasoning" not in parsed_output.intermediate_outputs 158 ): 159 raise RuntimeError( 160 "Reasoning is required for this model, but no reasoning was returned." 161 ) 162 163 # Generate the run and output 164 run = self.generate_run(input, input_source, parsed_output, usage) 165 166 # Save the run if configured to do so, and we have a path to save to 167 if ( 168 self.base_adapter_config.allow_saving 169 and Config.shared().autosave_runs 170 and self.task().path is not None 171 ): 172 run.save_to_file() 173 else: 174 # Clear the ID to indicate it's not persisted 175 run.id = None 176 177 return run, run_output
190 def build_prompt(self) -> str: 191 # The prompt builder needs to know if we want to inject formatting instructions 192 provider = self.model_provider() 193 add_json_instructions = self.has_structured_output() and ( 194 provider.structured_output_mode == StructuredOutputMode.json_instructions 195 or provider.structured_output_mode 196 == StructuredOutputMode.json_instruction_and_object 197 ) 198 199 return self.prompt_builder.build_prompt( 200 include_json_instructions=add_json_instructions 201 )
203 def run_strategy( 204 self, 205 ) -> Tuple[Literal["cot_as_message", "cot_two_call", "basic"], str | None]: 206 # Determine the run strategy for COT prompting. 3 options: 207 # 1. "Thinking" LLM designed to output thinking in a structured format plus a COT prompt: we make 1 call to the LLM, which outputs thinking in a structured format. We include the thinking instuctions as a message. 208 # 2. Normal LLM with COT prompt: we make 2 calls to the LLM - one for thinking and one for the final response. This helps us use the LLM's structured output modes (json_schema, tools, etc), which can't be used in a single call. It also separates the thinking from the final response. 209 # 3. Non chain of thought: we make 1 call to the LLM, with no COT prompt. 210 cot_prompt = self.prompt_builder.chain_of_thought_prompt() 211 reasoning_capable = self.model_provider().reasoning_capable 212 213 if cot_prompt and reasoning_capable: 214 # 1: "Thinking" LLM designed to output thinking in a structured format 215 # A simple message with the COT prompt appended to the message list is sufficient 216 return "cot_as_message", cot_prompt 217 elif cot_prompt: 218 # 2: Unstructured output with COT 219 # Two calls to separate the thinking from the final response 220 return "cot_two_call", cot_prompt 221 else: 222 return "basic", None
225 def generate_run( 226 self, 227 input: Dict | str, 228 input_source: DataSource | None, 229 run_output: RunOutput, 230 usage: Usage | None = None, 231 ) -> TaskRun: 232 # Convert input and output to JSON strings if they are dictionaries 233 input_str = ( 234 json.dumps(input, ensure_ascii=False) if isinstance(input, dict) else input 235 ) 236 output_str = ( 237 json.dumps(run_output.output, ensure_ascii=False) 238 if isinstance(run_output.output, dict) 239 else run_output.output 240 ) 241 242 # If no input source is provided, use the human data source 243 if input_source is None: 244 input_source = DataSource( 245 type=DataSourceType.human, 246 properties={"created_by": Config.shared().user_id}, 247 ) 248 249 new_task_run = TaskRun( 250 parent=self.task(), 251 input=input_str, 252 input_source=input_source, 253 output=TaskOutput( 254 output=output_str, 255 # Synthetic since an adapter, not a human, is creating this 256 source=DataSource( 257 type=DataSourceType.synthetic, 258 properties=self._properties_for_task_output(), 259 ), 260 ), 261 intermediate_outputs=run_output.intermediate_outputs, 262 tags=self.base_adapter_config.default_tags or [], 263 usage=usage, 264 ) 265 266 return new_task_run