pi-durable-python 1.3.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 (90) hide show
  1. pi_durable/__init__.py +320 -0
  2. pi_durable/context.py +328 -0
  3. pi_durable/delta.py +269 -0
  4. pi_durable/documents.py +366 -0
  5. pi_durable/entries.py +66 -0
  6. pi_durable/env/__init__.py +408 -0
  7. pi_durable/env/decode.py +48 -0
  8. pi_durable/env/line_scan.py +190 -0
  9. pi_durable/env/posix.py +1547 -0
  10. pi_durable/errors.py +89 -0
  11. pi_durable/harness/__init__.py +1 -0
  12. pi_durable/harness/agent.py +344 -0
  13. pi_durable/harness/compaction.py +673 -0
  14. pi_durable/harness/context.py +515 -0
  15. pi_durable/harness/define.py +111 -0
  16. pi_durable/harness/events.py +766 -0
  17. pi_durable/harness/generation.py +1079 -0
  18. pi_durable/harness/harness.py +570 -0
  19. pi_durable/harness/inbox.py +255 -0
  20. pi_durable/harness/live.py +374 -0
  21. pi_durable/harness/output.py +467 -0
  22. pi_durable/harness/prompt.py +288 -0
  23. pi_durable/harness/provider.py +78 -0
  24. pi_durable/harness/registry.py +136 -0
  25. pi_durable/harness/scheduler.py +1977 -0
  26. pi_durable/harness/submissions.py +357 -0
  27. pi_durable/harness/task_graph.py +334 -0
  28. pi_durable/harness/tool.py +1365 -0
  29. pi_durable/harness/types.py +772 -0
  30. pi_durable/harness/usage.py +88 -0
  31. pi_durable/harness/util.py +204 -0
  32. pi_durable/harness/validation.py +460 -0
  33. pi_durable/harness/view.py +324 -0
  34. pi_durable/ids.py +63 -0
  35. pi_durable/json.py +186 -0
  36. pi_durable/providers/__init__.py +362 -0
  37. pi_durable/providers/anthropic.py +603 -0
  38. pi_durable/providers/base.py +97 -0
  39. pi_durable/providers/json_parse.py +117 -0
  40. pi_durable/providers/openai.py +574 -0
  41. pi_durable/providers/request.py +35 -0
  42. pi_durable/providers/retry.py +127 -0
  43. pi_durable/providers/stream.py +183 -0
  44. pi_durable/providers/transcript.py +224 -0
  45. pi_durable/providers/usage.py +58 -0
  46. pi_durable/py.typed +0 -0
  47. pi_durable/search.py +115 -0
  48. pi_durable/session/__init__.py +5 -0
  49. pi_durable/session/drafts.py +762 -0
  50. pi_durable/session/forks.py +109 -0
  51. pi_durable/session/observation.py +374 -0
  52. pi_durable/session/session.py +1193 -0
  53. pi_durable/session/transaction.py +1088 -0
  54. pi_durable/storage/__init__.py +5 -0
  55. pi_durable/storage/jsonl/__init__.py +15 -0
  56. pi_durable/storage/jsonl/storage.py +958 -0
  57. pi_durable/storage/memory.py +889 -0
  58. pi_durable/storage/scan.py +57 -0
  59. pi_durable/storage/sqlite/__init__.py +26 -0
  60. pi_durable/storage/sqlite/database.py +62 -0
  61. pi_durable/storage/sqlite/migrations.py +191 -0
  62. pi_durable/storage/sqlite/posix.py +314 -0
  63. pi_durable/storage/sqlite/schema.py +97 -0
  64. pi_durable/storage/sqlite/storage.py +1130 -0
  65. pi_durable/tasks.py +89 -0
  66. pi_durable/testing/__init__.py +41 -0
  67. pi_durable/testing/assertions.py +44 -0
  68. pi_durable/testing/env_conformance.py +828 -0
  69. pi_durable/testing/runner.py +64 -0
  70. pi_durable/testing/storage_conformance.py +2008 -0
  71. pi_durable/testing/types.py +99 -0
  72. pi_durable/tool_search/__init__.py +159 -0
  73. pi_durable/tools/__init__.py +81 -0
  74. pi_durable/tools/_values.py +43 -0
  75. pi_durable/tools/bash.py +250 -0
  76. pi_durable/tools/edit.py +214 -0
  77. pi_durable/tools/edit_diff.py +452 -0
  78. pi_durable/tools/env.py +19 -0
  79. pi_durable/tools/file_mutation_queue.py +68 -0
  80. pi_durable/tools/image.py +248 -0
  81. pi_durable/tools/image_processor.py +77 -0
  82. pi_durable/tools/path_utils.py +111 -0
  83. pi_durable/tools/read.py +506 -0
  84. pi_durable/tools/write.py +71 -0
  85. pi_durable/truncate.py +185 -0
  86. pi_durable/types.py +1297 -0
  87. pi_durable_python-1.3.0.dist-info/METADATA +276 -0
  88. pi_durable_python-1.3.0.dist-info/RECORD +90 -0
  89. pi_durable_python-1.3.0.dist-info/WHEEL +4 -0
  90. pi_durable_python-1.3.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,460 @@
1
+ """Tool-argument repair, coercion, and validation.
2
+
3
+ Port of the subset of pi-ai 1.3.0 ``utils/validation.ts`` the tool task needs. Upstream validates
4
+ against a TypeBox schema; the Python port validates against the JSON Schema the tool declares,
5
+ coercing primitives the way pi-ai does before the first validation, and removing optional
6
+ properties a provider sent as ``null``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import copy
12
+ import json
13
+ import math
14
+ from collections import OrderedDict
15
+ from collections.abc import Mapping
16
+ from typing import Any
17
+
18
+ from ..json import copy_json
19
+
20
+ _JSON_TYPES = {"number", "integer", "boolean", "string", "null", "array", "object"}
21
+
22
+ _MESSAGES = {
23
+ "number": "Expected number",
24
+ "integer": "Expected integer",
25
+ "boolean": "Expected boolean",
26
+ "string": "Expected string",
27
+ "null": "Expected null",
28
+ "array": "Expected array",
29
+ "object": "Expected object",
30
+ }
31
+
32
+
33
+ class ToolArgumentError(ValueError):
34
+ """Invalid arguments for a tool call, with the message the model reads."""
35
+
36
+
37
+ def validate_tool_arguments(tool: Any, call: Mapping[str, Any]) -> dict[str, Any]:
38
+ """Validate a call's arguments against the tool's schema, returning the coerced arguments.
39
+
40
+ Raises :class:`ToolArgumentError` with the formatted message pi-ai throws.
41
+ """
42
+
43
+ name = str(call.get("name") or getattr(tool, "name", ""))
44
+ if call.get("argumentsError") is not None:
45
+ raise ToolArgumentError(f'Invalid arguments for tool "{name}": {call["argumentsError"]}')
46
+ schema = getattr(tool, "parameters", None) or {}
47
+ original = call.get("arguments")
48
+ args = copy.deepcopy(copy_json(original if original is not None else {}))
49
+ if isinstance(args, dict):
50
+ _normalize_optional_nulls(args, schema)
51
+ args = _coerce(args, schema)
52
+ if isinstance(args, dict) and _errors(args, schema) == []:
53
+ return args
54
+ errors = _errors(args, schema) or [("root", "Expected object")]
55
+ detail = "\n".join(f" - {path}: {message}" for path, message in errors)
56
+ raise ToolArgumentError(
57
+ f'Validation failed for tool "{name}":\n{detail}\n\n'
58
+ f"Received arguments:\n{json.dumps(original, indent=2)}"
59
+ )
60
+
61
+
62
+ # ── Validation ──────────────────────────────────────────────────────────────
63
+
64
+
65
+ def _path_of(instance_path: str) -> str:
66
+ """Format a JSON Pointer as a dotted path; the root is ``root``."""
67
+
68
+ path = instance_path.lstrip("/").replace("/", ".")
69
+ return path or "root"
70
+
71
+
72
+ def _errors(value: Any, schema: Any, instance_path: str = "") -> list[tuple[str, str]]:
73
+ if not isinstance(schema, Mapping):
74
+ return []
75
+ found: list[tuple[str, str]] = []
76
+ if isinstance(schema.get("allOf"), list):
77
+ for nested in schema["allOf"]:
78
+ found.extend(_errors(value, nested, instance_path))
79
+ for keyword in ("anyOf", "oneOf"):
80
+ alternatives = schema.get(keyword)
81
+ if not isinstance(alternatives, list):
82
+ continue
83
+ passed = sum(1 for nested in alternatives if not _errors(value, nested, instance_path))
84
+ wanted = 1 if keyword == "oneOf" else 1
85
+ if passed < wanted:
86
+ found.append((_path_of(instance_path), "Expected union value"))
87
+ if "const" in schema and value != schema["const"]:
88
+ found.append((_path_of(instance_path), f"Expected {json.dumps(schema['const'])}"))
89
+ if isinstance(schema.get("enum"), list) and value not in schema["enum"]:
90
+ options = ", ".join(json.dumps(item) for item in schema["enum"])
91
+ found.append((_path_of(instance_path), f"Expected one of {options}"))
92
+ types = _schema_types(schema)
93
+ if types and not any(_matches(value, item) for item in types):
94
+ expected = " | ".join(types) if len(types) > 1 else types[0]
95
+ found.append((_path_of(instance_path), _MESSAGES.get(types[0], f"Expected {expected}")))
96
+ return found
97
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
98
+ found.extend(_number_errors(value, schema, instance_path))
99
+ if isinstance(value, str):
100
+ found.extend(_string_errors(value, schema, instance_path))
101
+ if isinstance(value, list):
102
+ found.extend(_array_errors(value, schema, instance_path))
103
+ if isinstance(value, dict):
104
+ found.extend(_object_errors(value, schema, instance_path))
105
+ return found
106
+
107
+
108
+ def _schema_types(schema: Mapping[str, Any]) -> list[str]:
109
+ declared = schema.get("type")
110
+ if isinstance(declared, str):
111
+ return [declared] if declared in _JSON_TYPES else []
112
+ if isinstance(declared, list):
113
+ return [item for item in declared if isinstance(item, str) and item in _JSON_TYPES]
114
+ return []
115
+
116
+
117
+ def _matches(value: Any, json_type: str) -> bool:
118
+ if json_type == "number":
119
+ return isinstance(value, (int, float)) and not isinstance(value, bool)
120
+ if json_type == "integer":
121
+ return isinstance(value, int) and not isinstance(value, bool)
122
+ if json_type == "boolean":
123
+ return isinstance(value, bool)
124
+ if json_type == "string":
125
+ return isinstance(value, str)
126
+ if json_type == "null":
127
+ return value is None
128
+ if json_type == "array":
129
+ return isinstance(value, list)
130
+ if json_type == "object":
131
+ return isinstance(value, dict)
132
+ return False
133
+
134
+
135
+ def _number_errors(value: float, schema: Mapping[str, Any], path: str) -> list[tuple[str, str]]:
136
+ found: list[tuple[str, str]] = []
137
+ minimum = schema.get("minimum")
138
+ if isinstance(minimum, (int, float)) and value < minimum:
139
+ found.append((_path_of(path), f"Expected number to be greater or equal to {minimum}"))
140
+ maximum = schema.get("maximum")
141
+ if isinstance(maximum, (int, float)) and value > maximum:
142
+ found.append((_path_of(path), f"Expected number to be less or equal to {maximum}"))
143
+ exclusive_minimum = schema.get("exclusiveMinimum")
144
+ if isinstance(exclusive_minimum, (int, float)) and value <= exclusive_minimum:
145
+ found.append((_path_of(path), f"Expected number to be greater than {exclusive_minimum}"))
146
+ exclusive_maximum = schema.get("exclusiveMaximum")
147
+ if isinstance(exclusive_maximum, (int, float)) and value >= exclusive_maximum:
148
+ found.append((_path_of(path), f"Expected number to be less than {exclusive_maximum}"))
149
+ multiple = schema.get("multipleOf")
150
+ if isinstance(multiple, (int, float)) and multiple > 0:
151
+ quotient = value / multiple
152
+ if not math.isclose(quotient, round(quotient), abs_tol=1e-9):
153
+ found.append((_path_of(path), f"Expected number to be a multiple of {multiple}"))
154
+ return found
155
+
156
+
157
+ def _string_errors(value: str, schema: Mapping[str, Any], path: str) -> list[tuple[str, str]]:
158
+ import re
159
+
160
+ found: list[tuple[str, str]] = []
161
+ min_length = schema.get("minLength")
162
+ if isinstance(min_length, int) and len(value) < min_length:
163
+ found.append((_path_of(path), f"Expected string to have a minimum length of {min_length}"))
164
+ max_length = schema.get("maxLength")
165
+ if isinstance(max_length, int) and len(value) > max_length:
166
+ found.append((_path_of(path), f"Expected string to have a maximum length of {max_length}"))
167
+ pattern = schema.get("pattern")
168
+ if isinstance(pattern, str):
169
+ try:
170
+ matched = re.search(pattern, value) is not None
171
+ except re.error:
172
+ matched = True
173
+ if not matched:
174
+ found.append((_path_of(path), f"Expected string to match '{pattern}'"))
175
+ return found
176
+
177
+
178
+ def _array_errors(value: list[Any], schema: Mapping[str, Any], path: str) -> list[tuple[str, str]]:
179
+ found: list[tuple[str, str]] = []
180
+ min_items = schema.get("minItems")
181
+ if isinstance(min_items, int) and len(value) < min_items:
182
+ found.append((_path_of(path), f"Expected array to have at least {min_items} items"))
183
+ max_items = schema.get("maxItems")
184
+ if isinstance(max_items, int) and len(value) > max_items:
185
+ found.append((_path_of(path), f"Expected array to have at most {max_items} items"))
186
+ if schema.get("uniqueItems") is True:
187
+ seen: list[Any] = []
188
+ for item in value:
189
+ if item in seen:
190
+ found.append((_path_of(path), "Expected array items to be unique"))
191
+ break
192
+ seen.append(item)
193
+ items = schema.get("items")
194
+ if isinstance(items, list):
195
+ for index, item_schema in enumerate(items):
196
+ if index < len(value):
197
+ found.extend(_errors(value[index], item_schema, f"{path}/{index}"))
198
+ elif isinstance(items, Mapping):
199
+ for index, item in enumerate(value):
200
+ found.extend(_errors(item, items, f"{path}/{index}"))
201
+ return found
202
+
203
+
204
+ def _object_errors(value: dict[str, Any], schema: Mapping[str, Any], path: str) -> list[tuple[str, str]]:
205
+ found: list[tuple[str, str]] = []
206
+ properties = schema.get("properties")
207
+ properties = properties if isinstance(properties, Mapping) else {}
208
+ required = schema.get("required")
209
+ for name in required if isinstance(required, list) else []:
210
+ if name not in value:
211
+ found.append((_path_of(f"{path}/{name}"), "Expected required property"))
212
+ for name, item in value.items():
213
+ item_path = f"{path}/{name}"
214
+ if name in properties:
215
+ found.extend(_errors(item, properties[name], item_path))
216
+ continue
217
+ additional = schema.get("additionalProperties")
218
+ if additional is False:
219
+ found.append((_path_of(item_path), "Expected not to have additional properties"))
220
+ elif isinstance(additional, Mapping):
221
+ found.extend(_errors(item, additional, item_path))
222
+ min_properties = schema.get("minProperties")
223
+ if isinstance(min_properties, int) and len(value) < min_properties:
224
+ found.append((_path_of(path), f"Expected object to have at least {min_properties} properties"))
225
+ max_properties = schema.get("maxProperties")
226
+ if isinstance(max_properties, int) and len(value) > max_properties:
227
+ found.append((_path_of(path), f"Expected object to have at most {max_properties} properties"))
228
+ return found
229
+
230
+
231
+ # ── Coercion ────────────────────────────────────────────────────────────────
232
+
233
+
234
+ def _normalize_optional_nulls(value: Any, schema: Any) -> None:
235
+ """Remove optional properties a provider sent as ``null``, as pi-ai does before validation."""
236
+
237
+ if not isinstance(schema, Mapping):
238
+ return
239
+ if isinstance(value, list):
240
+ items = schema.get("items")
241
+ if isinstance(items, list):
242
+ for index, item in enumerate(value):
243
+ if index < len(items):
244
+ _normalize_optional_nulls(item, items[index])
245
+ elif isinstance(items, Mapping):
246
+ for item in value:
247
+ _normalize_optional_nulls(item, items)
248
+ return
249
+ if not isinstance(value, dict):
250
+ return
251
+ properties = schema.get("properties")
252
+ if not isinstance(properties, Mapping):
253
+ return
254
+ required = set(schema.get("required") or [])
255
+ for name, property_schema in properties.items():
256
+ if name not in value:
257
+ continue
258
+ if (
259
+ value[name] is None
260
+ and name not in required
261
+ and isinstance(property_schema, Mapping)
262
+ and not isinstance(property_schema.get("$ref"), str)
263
+ and _errors(None, property_schema) != []
264
+ ):
265
+ del value[name]
266
+ else:
267
+ _normalize_optional_nulls(value[name], property_schema)
268
+
269
+
270
+ def _coerce(value: Any, schema: Any) -> Any:
271
+ if not isinstance(schema, Mapping):
272
+ return value
273
+ if isinstance(schema.get("allOf"), list):
274
+ for nested in schema["allOf"]:
275
+ value = _coerce(value, nested)
276
+ for keyword in ("anyOf", "oneOf"):
277
+ alternatives = schema.get(keyword)
278
+ if isinstance(alternatives, list):
279
+ value = _coerce_union(value, alternatives)
280
+ types = _schema_types(schema)
281
+ matches_union_member = len(types) > 1 and any(_matches(value, item) for item in types)
282
+ if types and not matches_union_member:
283
+ for json_type in types:
284
+ candidate = _coerce_primitive(value, json_type)
285
+ if candidate is not value:
286
+ value = candidate
287
+ break
288
+ if "object" in types and isinstance(value, dict):
289
+ _coerce_object(value, schema)
290
+ if "array" in types and isinstance(value, list):
291
+ _coerce_array(value, schema)
292
+ return value
293
+
294
+
295
+ def _coerce_union(value: Any, alternatives: list[Any]) -> Any:
296
+ if any(not _errors(value, schema) for schema in alternatives):
297
+ return value
298
+ for schema in alternatives:
299
+ candidate = copy.deepcopy(value)
300
+ coerced = _coerce(candidate, schema)
301
+ if not _errors(coerced, schema):
302
+ return coerced
303
+ return value
304
+
305
+
306
+ def _coerce_object(value: dict[str, Any], schema: Mapping[str, Any]) -> None:
307
+ properties = schema.get("properties")
308
+ defined = set(properties or {}) if isinstance(properties, Mapping) else set()
309
+ if isinstance(properties, Mapping):
310
+ for name, property_schema in properties.items():
311
+ if name in value:
312
+ value[name] = _coerce(value[name], property_schema)
313
+ additional = schema.get("additionalProperties")
314
+ if isinstance(additional, Mapping):
315
+ for name, item in value.items():
316
+ if name not in defined:
317
+ value[name] = _coerce(item, additional)
318
+
319
+
320
+ def _coerce_array(value: list[Any], schema: Mapping[str, Any]) -> None:
321
+ items = schema.get("items")
322
+ if isinstance(items, list):
323
+ for index, item_schema in enumerate(items):
324
+ if index < len(value):
325
+ value[index] = _coerce(value[index], item_schema)
326
+ return
327
+ if isinstance(items, Mapping):
328
+ for index, item in enumerate(value):
329
+ value[index] = _coerce(item, items)
330
+
331
+
332
+ def _coerce_primitive(value: Any, json_type: str) -> Any:
333
+ if json_type == "number":
334
+ if value is None:
335
+ return 0
336
+ if isinstance(value, str) and value.strip() != "":
337
+ try:
338
+ parsed = float(value)
339
+ except ValueError:
340
+ return value
341
+ if math.isfinite(parsed):
342
+ return int(parsed) if parsed.is_integer() else parsed
343
+ if isinstance(value, bool) and type(value) is bool:
344
+ return 1 if value else 0
345
+ return value
346
+ if json_type == "integer":
347
+ if value is None:
348
+ return 0
349
+ if isinstance(value, str) and value.strip() != "":
350
+ try:
351
+ parsed = float(value)
352
+ except ValueError:
353
+ return value
354
+ if math.isfinite(parsed) and parsed.is_integer():
355
+ return int(parsed)
356
+ if isinstance(value, bool) and type(value) is bool:
357
+ return 1 if value else 0
358
+ return value
359
+ if json_type == "boolean":
360
+ if value is None:
361
+ return False
362
+ if value == "true":
363
+ return True
364
+ if value == "false":
365
+ return False
366
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
367
+ if value == 1:
368
+ return True
369
+ if value == 0:
370
+ return False
371
+ return value
372
+ if json_type == "string":
373
+ if value is None:
374
+ return ""
375
+ if isinstance(value, bool):
376
+ return "true" if value else "false"
377
+ if isinstance(value, (int, float)):
378
+ return _js_string(value)
379
+ return value
380
+ if json_type == "null":
381
+ if value == "" or (isinstance(value, int) and not isinstance(value, bool) and value == 0):
382
+ return None
383
+ if value is False:
384
+ return None
385
+ return value
386
+ return value
387
+
388
+
389
+ def _js_string(value: int | float) -> str:
390
+ """JavaScript ``String(number)``: integral floats lose their fraction, ``1e21`` is exponential."""
391
+
392
+ if isinstance(value, int):
393
+ return str(value)
394
+ if value.is_integer() and abs(value) < 1e21:
395
+ return str(int(value))
396
+ return repr(value)
397
+
398
+
399
+ _VALIDATOR_CACHE_SIZE = 64
400
+
401
+ _STRUCTURED_VALIDATORS: OrderedDict[int, tuple[Any, Any]] = OrderedDict()
402
+ """Compiled validators keyed by the identity of the schema object they validate, least recently used
403
+ first, so a long-lived process with many short-lived tools does not accumulate them."""
404
+
405
+
406
+ def structured_output_error(tool: Any, result: Mapping[str, Any]) -> str | None:
407
+ """Why a result's ``structuredOutput`` breaks the tool's contract, or ``None`` when it keeps it.
408
+
409
+ A tool that declares ``structured_output_schema`` must return a matching value, except on an
410
+ error result, where it may omit it; a tool without one must not return one. Validation needs
411
+ the optional ``schema`` extra, imported only here so the core package has no dependencies.
412
+ """
413
+
414
+ name = str(getattr(tool, "name", ""))
415
+ schema = getattr(tool, "structured_output_schema", None)
416
+ present = "structuredOutput" in result
417
+ value = result.get("structuredOutput")
418
+ if schema is None:
419
+ if not present:
420
+ return None
421
+ return f"Tool {name} returned structuredOutput but declares no structuredOutputSchema"
422
+ if not present:
423
+ if result.get("isError") is True:
424
+ return None
425
+ return f"Tool {name} returned no structuredOutput"
426
+ validator = _structured_validator(schema)
427
+ errors = list(validator.iter_errors(value))
428
+ if not errors:
429
+ return None
430
+ first = errors[0]
431
+ path = ".".join(str(item) for item in first.absolute_path) or "root"
432
+ return f"Tool {name} returned structuredOutput that does not match its schema: {path}: {first.message}"
433
+
434
+
435
+ def _structured_validator(schema: Any) -> Any:
436
+ cached = _STRUCTURED_VALIDATORS.get(id(schema))
437
+ if cached is not None and cached[0] is schema:
438
+ _STRUCTURED_VALIDATORS.move_to_end(id(schema))
439
+ return cached[1]
440
+ try:
441
+ from jsonschema import Draft202012Validator # type: ignore[import-untyped]
442
+ except ImportError as error:
443
+ # The core package keeps no runtime dependency; only a schema tool needs the extra.
444
+ raise ImportError(
445
+ "JSON Schema validation requires the 'schema' extra: "
446
+ "install pi-durable-python[schema]"
447
+ ) from error
448
+ validator = Draft202012Validator(schema)
449
+ _STRUCTURED_VALIDATORS[id(schema)] = (schema, validator)
450
+ _STRUCTURED_VALIDATORS.move_to_end(id(schema))
451
+ while len(_STRUCTURED_VALIDATORS) > _VALIDATOR_CACHE_SIZE:
452
+ _STRUCTURED_VALIDATORS.popitem(last=False)
453
+ return validator
454
+
455
+
456
+ __all__ = [
457
+ "ToolArgumentError",
458
+ "structured_output_error",
459
+ "validate_tool_arguments",
460
+ ]