diff --git a/effectful/handlers/llm/harness/observability/rich.py b/effectful/handlers/llm/harness/observability/rich.py
index 2987cdb20..8c81a18f1 100644
--- a/effectful/handlers/llm/harness/observability/rich.py
+++ b/effectful/handlers/llm/harness/observability/rich.py
@@ -240,9 +240,9 @@ def _is_python(text: str, *, partial: bool = False) -> bool:
"""Whether `text` looks like a Python source snippet worth highlighting.
Detects code by *content* rather than schema/field name, so it covers every
- `Encodable` type that serializes Python as a string -- the synthesis
- `SynthesizedFunction.code` field, `exec_code`'s `types.CodeType`
- argument, and any future code-carrying tool -- uniformly. Requires a
+ `Encodable` type that serializes Python as a string -- a synthesized
+ `Callable`, `exec_code`'s `types.CodeType` argument, and any future
+ code-carrying tool -- uniformly. Requires a
multi-line string that parses as a module with at least one real statement
(not a lone expression), which excludes prose and JSON-as-string.
@@ -261,8 +261,8 @@ def _is_python(text: str, *, partial: bool = False) -> bool:
def _extract_code(args: typing.Any, *, partial: bool = False) -> str | None:
"""Return an embedded Python source string from parsed tool-call arguments.
- Walks nested dicts (a synthesized callable is ``{"implementation":
- {"code": ...}}``; `exec_code` is a flat ``{"code": ...}``) and returns
+ Walks nested dicts (a synthesized callable is ``{"implementation": ...}``;
+ `exec_code` is ``{"code": ...}``) and returns
the first string value that :func:`_is_python` recognizes. ``partial`` is
forwarded, and is set while streaming, where the payload is source cut
mid-line and has to be judged as a prefix rather than as a module.
diff --git a/effectful/handlers/llm/harness/serialization.py b/effectful/handlers/llm/harness/serialization.py
index 3168f7bc4..a1a229800 100644
--- a/effectful/handlers/llm/harness/serialization.py
+++ b/effectful/handlers/llm/harness/serialization.py
@@ -712,32 +712,25 @@ def _pydantic_type_image(ty: type[Image.Image]):
]
-# The *serialization* view of a synthesized callable: the shape the model reads
-# when a function is handed to it as a value (e.g. a tool's return) -- just the
-# source, with none of the synthesis instructions the `SynthesizedFunction` subtype
-# carries for the generation direction. Its JSON schema (docstring included, since
-# pydantic renders it as the schema `description`) is what every synthesized-callable
-# encoding serializes as, so keep the docstring model-facing.
-class EncodedFunction(pydantic.BaseModel):
- """A function, encoded as a string of its complete Python source."""
-
- code: str = pydantic.Field(..., description="Python source defining the function.")
-
-
-def _serialize_callable(value: collections.abc.Callable) -> EncodedFunction:
- """Encode a callable back to its ``code`` form (source, or a stub).
-
- Emits a plain `EncodedFunction` -- which is exactly what the serialization JSON
- schema declares -- rather than the `SynthesizedFunction` subclass that governs
- the *other* direction. The two directions carry different obligations, and
- conflating them was a real bug: `SynthesizedFunction`'s constraints ("the last
- statement must be a function definition", "every parameter must be annotated")
- are demands on code a model is *writing*, and a value being serialized is under
- no such obligation. It is any callable that reached this point -- a class handed
- back by a lexical-scope read, or an inner function a Skill *body* returned,
- which was never required to annotate anything -- and re-validating its recovered
- source rejected those with a `ValidationError` raised from inside pydantic's
- serializer, aborting the whole call rather than encoding the value.
+# The *serialization* view of a synthesized callable: the shape the model reads when
+# a function is handed to it as a value (e.g. a tool's return) -- the bare source,
+# with none of the synthesis instructions the validation direction carries.
+EncodedFunction = typing.Annotated[
+ str,
+ pydantic.Field(
+ description="A function, as a string of its complete Python source."
+ ),
+]
+
+
+def _serialize_callable(value: collections.abc.Callable) -> str:
+ """Encode a callable as its source, or as a stub when there is none.
+
+ The synthesis constraints ("the last statement must be a function definition",
+ "every parameter must be annotated") are demands on code a model is *writing*;
+ a value being serialized is under no such obligation -- it may be a class from a
+ lexical-scope read, or an inner function a Skill body returned -- so they are
+ deliberately not re-applied here.
"""
try:
source = inspect.getsource(value)
@@ -745,7 +738,7 @@ def _serialize_callable(value: collections.abc.Callable) -> EncodedFunction:
source = None
if source:
- return EncodedFunction(code=textwrap.dedent(source))
+ return textwrap.dedent(source)
name = getattr(value, "__name__", None)
docstring = inspect.getdoc(value)
@@ -759,11 +752,10 @@ def _serialize_callable(value: collections.abc.Callable) -> EncodedFunction:
except (ValueError, TypeError):
sig_str = "(...)"
- stub_code = f'''def {name}{sig_str}:
+ return f'''def {name}{sig_str}:
"""{docstring}"""
...
'''
- return EncodedFunction(code=stub_code)
@TypeToPydanticType.register(collections.abc.Callable)
diff --git a/effectful/handlers/llm/harness/synthesis/body.py b/effectful/handlers/llm/harness/synthesis/body.py
index cc23430c6..0e0f2f48f 100644
--- a/effectful/handlers/llm/harness/synthesis/body.py
+++ b/effectful/handlers/llm/harness/synthesis/body.py
@@ -38,7 +38,6 @@
import collections.abc
import functools
import inspect
-import textwrap
import types
import typing
from collections.abc import Callable
@@ -69,9 +68,10 @@
)
from effectful.handlers.llm.harness.synthesis.function import (
SplicedRegion,
- SynthesizedFunction,
+ _checked_source,
_def_nodes,
_recover_skill_def,
+ _synthesized_source_schema,
)
from effectful.handlers.llm.types import Encodable, Skill, Tool
from effectful.ops.semantics import fwd, handler
@@ -177,40 +177,20 @@ def __class_getitem__(cls, item):
return types.GenericAlias(cls, item)
-class SynthesizedSkillBody(SynthesizedFunction):
- """Structured output for synthesizing a `Skill`'s body (`write_and_run_body`).
+# A Skill body is type-checked against the Skill's own (already-annotated) signature
+# by `_splice_body`, so the synthesized body's own annotations are optional.
+_SKILL_BODY_CONSTRAINTS: list[str] = [
+ "Write the function with the Skill's signature; parameter and return "
+ "annotations are optional.",
+ "Do not include a docstring or doctests; the Skill's are supplied automatically.",
+]
- Decoded through `_pydantic_skill_body`: the function is type-checked against
- the enclosing Skill's source and its doctests are run with self/recursive
- calls routed to the synthesized implementation.
-
- Unlike `SynthesizedFunction`, the parameter and return *annotations* are not
- required: a Skill body is type-checked against the Skill's own signature
- (see `splice_skill_body`), so the model may omit or vary them -- in
- particular it need not annotate the ``self`` receiver of an instance-method
- Skill.
- """
-
- code: str = pydantic.Field(
- ...,
- description=textwrap.dedent("""
- The complete Python source implementing the Skill shown in its spec.
- The code MUST satisfy the following constraints, or it will fail validation:
-
-
- 1. The code MUST be one complete syntactically valid Python module.
- 2. The code MUST NOT use star imports or ``__future__`` imports.
- 3. The function definition MUST be the LAST statement - do not add any code after it.
- 4. Write the function with the Skill's signature; parameter and return
- annotations are optional.
- 5. Do not include a docstring or doctests; the Skill's are supplied automatically.
-
- """),
- )
-
- # A Skill body is checked against the Skill's own (already-annotated)
- # signature, so the synthesized body's annotations are optional.
- _require_annotations: typing.ClassVar[bool] = False
+_METHOD_SKILL_BODY_CONSTRAINTS: list[str] = [
+ "Write the function with the Skill's signature: its FIRST parameter is the "
+ "instance receiver ``self`` (which you may leave unannotated); all other "
+ "parameter and return annotations are optional too.",
+ "Do not include a docstring or doctests; the Skill's are supplied automatically.",
+]
@TypeToPydanticType.register(SkillBody)
@@ -223,20 +203,20 @@ def _pydantic_skill_body(ty: typing.Any) -> typing.Any:
implementation, so a doctest that calls the Skill (including for recursion)
exercises the freshly synthesized code rather than re-invoking the model.
"""
- typed_enc = SynthesizedSkillBody._create_model_from_callable_type(
+ schema = _synthesized_source_schema(
ty if typing.get_args(ty) else Callable[..., typing.Any], # type: ignore[arg-type]
+ "the Skill shown in its spec",
+ _SKILL_BODY_CONSTRAINTS,
)
def _validate(
- value: SynthesizedSkillBody | dict | str | Callable,
+ value: str | Callable,
info: pydantic.ValidationInfo,
) -> Callable:
- if isinstance(value, str):
- value = typed_enc.model_validate({"code": value})
- elif isinstance(value, dict):
- value = typed_enc.model_validate(value)
- elif callable(value):
+ value = _checked_source(value, require_annotations=False)
+ if not isinstance(value, str):
return typing.cast(Callable, value)
+
ctx = info.context or {}
anchor = ctx.get(_TYPE_CHECK_ANCHOR_KEY)
if anchor is not None:
@@ -244,9 +224,9 @@ def _validate(
assert isinstance(anchor, Skill)
ctx = anchor.__context__
- filename = f""
+ filename = f""
module: ast.Module = effectful.handlers.llm.harness.execution.hooks.parse(
- value.code, filename
+ value, filename
)
# `None` means the Skill's source can't be recovered (REPL/exec/notebook
@@ -282,7 +262,7 @@ def _validate(
return typing.Annotated[
pydantic.InstanceOf[ty_], # type: ignore
- pydantic.BeforeValidator(_validate, json_schema_input_type=typed_enc),
+ pydantic.BeforeValidator(_validate, json_schema_input_type=schema),
pydantic.PlainSerializer(_serialize_callable, return_type=EncodedFunction),
]
@@ -299,58 +279,6 @@ class MethodSkillBody(SkillBody):
"""
-class SynthesizedMethodSkillBody(SynthesizedSkillBody):
- """Structured output for synthesizing an *instance-method* `Skill`'s body.
-
- Decoded through `_pydantic_skill_body`: the function is type-checked against
- the enclosing Skill's source and its doctests are run with self/recursive
- calls routed to the synthesized implementation.
-
- Unlike `SynthesizedFunction`, the parameter and return *annotations* are not
- required: a Skill body is type-checked against the Skill's own signature
- (see `splice_skill_body`), so the model may omit or vary them -- in
- particular it need not annotate the ``self`` receiver of an instance-method
- Skill.
- """
-
- code: str = pydantic.Field(
- ...,
- description=textwrap.dedent("""
- The complete Python source implementing the instance-method Skill shown in
- its spec. The code MUST satisfy the following constraints, or it will fail
- validation:
-
-
- 1. The code MUST be one complete syntactically valid Python module.
- 2. The code MUST NOT use star imports or ``__future__`` imports.
- 3. The function definition MUST be the LAST statement - do not add any code after it.
- 4. Write the function with the Skill's signature: its FIRST parameter is the
- instance receiver ``self`` (which you may leave unannotated); all other parameter
- and return annotations are optional too.
- 5. Do not include a docstring or doctests; the Skill's are supplied automatically.
-
- """),
- )
-
- @classmethod
- def _param_names(cls, param_types: typing.Iterable[typing.Any]) -> list[str]:
- # The method's callable type already carries the receiver as its first
- # parameter (with an uninformative Agent-class type); relabel it ``self`` so
- # the model reproduces it rather than inventing one -- do NOT prepend a receiver.
- names = super()._param_names(param_types)
- if names:
- names[0] = "self"
- return names
-
- @classmethod
- def _extra_instructions(cls) -> str:
- return (
- "\n\nThis implements an instance method: the first parameter is the "
- "instance receiver `self`. Include it as the first parameter; you may "
- "leave it unannotated."
- )
-
-
def _class_skill_of(op: typing.Any) -> typing.Any | None:
"""The class-level `Skill` underlying an Agent-method Skill ``op``.
@@ -382,20 +310,21 @@ def _pydantic_method_skill_body(ty: typing.Any) -> typing.Any:
their own instances -- route ``agent.method(...)`` on *any* instance to the
synthesized implementation.
"""
- typed_enc = SynthesizedMethodSkillBody._create_model_from_callable_type(
+ schema = _synthesized_source_schema(
ty if typing.get_args(ty) else Callable[..., typing.Any], # type: ignore[arg-type]
+ "the instance-method Skill shown in its spec",
+ _METHOD_SKILL_BODY_CONSTRAINTS,
+ receiver=True,
)
def _validate(
- value: SynthesizedMethodSkillBody | dict | str | Callable,
+ value: str | Callable,
info: pydantic.ValidationInfo,
) -> Callable:
- if isinstance(value, str):
- value = typed_enc.model_validate({"code": value})
- elif isinstance(value, dict):
- value = typed_enc.model_validate(value)
- elif callable(value):
+ value = _checked_source(value, require_annotations=False)
+ if not isinstance(value, str):
return typing.cast(Callable, value)
+
ctx = info.context or {}
anchor = ctx.get(_TYPE_CHECK_ANCHOR_KEY)
if anchor is not None:
@@ -403,9 +332,9 @@ def _validate(
assert isinstance(anchor, Skill)
ctx = anchor.__context__
- filename = f""
+ filename = f""
module: ast.Module = effectful.handlers.llm.harness.execution.hooks.parse(
- value.code, filename
+ value, filename
)
anchor_asts = _recover_skill_def(anchor) if anchor is not None else None
if anchor_asts is not None:
@@ -445,7 +374,7 @@ def _doctest_apply(op, *args, **kwargs):
return typing.Annotated[
pydantic.InstanceOf[ty_], # type: ignore
- pydantic.BeforeValidator(_validate, json_schema_input_type=typed_enc),
+ pydantic.BeforeValidator(_validate, json_schema_input_type=schema),
pydantic.PlainSerializer(_serialize_callable, return_type=EncodedFunction),
]
diff --git a/effectful/handlers/llm/harness/synthesis/function.py b/effectful/handlers/llm/harness/synthesis/function.py
index ce68958b1..8f2c4c112 100644
--- a/effectful/handlers/llm/harness/synthesis/function.py
+++ b/effectful/handlers/llm/harness/synthesis/function.py
@@ -3,7 +3,6 @@
import inspect
import linecache
import logging
-import textwrap
import types
import typing
@@ -210,160 +209,154 @@ def adder(x: int) -> int:
return checked_source, lo, hi
-class SynthesizedFunction(EncodedFunction):
- """
- Structured output for function synthesis.
- """
-
- code: str = pydantic.Field(
- ...,
- description=textwrap.dedent("""
- A string containing the complete Python source code for the function.
- The code MUST satisfy the following constraints, or it will fail validation:
-
-
- 1. The code MUST be one complete syntactically valid Python module.
- 2. The code MUST NOT use star imports or ``__future__`` imports.
- 3. The function definition MUST be the LAST statement - do not add any code after it.
- 4. The function MUST have type annotations for all parameters and the return type.
- 5. You may include doctest examples (lines starting with >>>) inside the function's
- docstring to demonstrate and verify its behavior; these examples are run as tests.
-
- """),
+# Constraints 1-3 below, shared by every synthesis site; each site appends its own.
+_COMMON_CONSTRAINTS: list[str] = [
+ "The code MUST be one complete syntactically valid Python module.",
+ "The code MUST NOT use star imports or ``__future__`` imports.",
+ "The function definition MUST be the LAST statement - do not add any code after it.",
+]
+
+_FUNCTION_CONSTRAINTS: list[str] = [
+ "The function MUST have type annotations for all parameters and the return type.",
+ "You may include doctest examples (lines starting with >>>) inside the function's "
+ "docstring to demonstrate and verify its behavior; these examples are run as tests.",
+]
+
+
+def _param_names(param_types: typing.Iterable[typing.Any], receiver: bool) -> list[str]:
+ names = [getattr(t, "__name__", str(t)) for t in param_types]
+ # A method's callable type already carries the receiver as its first parameter,
+ # under an uninformative Agent-class type; relabel it so the model reproduces
+ # it rather than inventing one.
+ if receiver and names:
+ names[0] = "self"
+ return names
+
+
+def _signature_str(
+ typ: type[collections.abc.Callable], *, receiver: bool = False
+) -> str:
+ """Render a ``Callable[[...], ...]`` signature by type *name* (not its
+ fully-qualified ``repr``), so the model sees ``Callable[[State], int]`` rather
+ than ``collections.abc.Callable[[pkg.mod.State], builtins.int]``."""
+ args = typing.get_args(typ)
+ if not args:
+ return "Callable"
+ param_types, return_type = args
+ params_str = (
+ "..." if param_types is ... else ", ".join(_param_names(param_types, receiver))
+ )
+ return_str = getattr(return_type, "__name__", str(return_type))
+ return f"Callable[[{params_str}], {return_str}]"
+
+
+def _synthesized_source_schema(
+ typ: type[collections.abc.Callable],
+ subject: str,
+ extra_constraints: collections.abc.Sequence[str],
+ *,
+ receiver: bool = False,
+) -> typing.Any:
+ """The validation-side annotation for synthesized source: a bare string carrying
+ the requested signature and the constraints it will be held to."""
+ constraints = "\n".join(
+ f"{i}. {c}" for i, c in enumerate([*_COMMON_CONSTRAINTS, *extra_constraints], 1)
)
+ return typing.Annotated[
+ str,
+ pydantic.Field(
+ description=(
+ f"The complete Python source for {subject}, with signature "
+ f"{_signature_str(typ, receiver=receiver)}.\n"
+ f"The code MUST satisfy the following constraints, or it will fail "
+ f"validation:\n\n\n{constraints}\n"
+ )
+ ),
+ ]
- # A general `Callable` is type-checked against the requested signature, so it must
- # be fully annotated. A Skill *body* is instead checked against the enclosing
- # Skill's own signature (`splice_skill_body`), which already carries the
- # annotations -- so its subclasses waive this and may omit the `self` receiver.
- _require_annotations: typing.ClassVar[bool] = True
- @pydantic.field_validator("code")
- @classmethod
- def _validate_code(cls, value: str) -> str:
- module: ast.AST = ast.parse(value)
+def _checked_source(
+ value: typing.Any, *, require_annotations: bool
+) -> str | collections.abc.Callable:
+ """The model's source, held to the constraints its schema states -- or an
+ already-decoded callable, passed through untouched."""
+ if not isinstance(value, str):
+ if callable(value):
+ return value
+ raise ValueError(
+ f"expected Python source as a string, got {type(value).__name__}"
+ )
+
+ module: ast.AST = ast.parse(value)
+
+ if not isinstance(module, ast.Module) or not module.body:
+ raise ValueError("decode() requires module code with at least one statement.")
+
+ last_stmt = module.body[-1]
+ if not isinstance(last_stmt, ast.FunctionDef):
+ raise ValueError(
+ f"decode() requires the last statement to be a function definition, "
+ f"got {type(last_stmt).__name__}"
+ )
- if not isinstance(module, ast.Module) or not module.body:
+ if require_annotations:
+ for arg in last_stmt.args.args:
+ if arg.annotation is None:
+ raise ValueError(
+ f"decode() requires all parameters to have type annotations, "
+ f"parameter '{arg.arg}' is missing an annotation"
+ )
+ if last_stmt.returns is None:
raise ValueError(
- "decode() requires module code with at least one statement."
+ "decode() requires the function to have a return type annotation"
)
- last_stmt = module.body[-1]
- if not isinstance(last_stmt, ast.FunctionDef):
+ for stmt in module.body:
+ if isinstance(stmt, ast.ImportFrom) and stmt.module == "__future__":
raise ValueError(
- f"decode() requires the last statement to be a function definition, "
- f"got {type(last_stmt).__name__}"
+ "decode() does not allow __future__ imports in the module code"
)
- if cls._require_annotations:
- for arg in last_stmt.args.args:
- if arg.annotation is None:
+ for stmt in module.body:
+ if isinstance(stmt, ast.ImportFrom) and stmt.names:
+ for alias in stmt.names:
+ if alias.name == "*":
raise ValueError(
- f"decode() requires all parameters to have type annotations, "
- f"parameter '{arg.arg}' is missing an annotation"
+ "decode() does not allow star imports in the module code"
)
- if last_stmt.returns is None:
- raise ValueError(
- "decode() requires the function to have a return type annotation"
- )
-
- for stmt in module.body:
- if isinstance(stmt, ast.ImportFrom) and stmt.module == "__future__":
- raise ValueError(
- "decode() does not allow __future__ imports in the module code"
- )
- for stmt in module.body:
- if isinstance(stmt, ast.ImportFrom) and stmt.names:
- for alias in stmt.names:
- if alias.name == "*":
- raise ValueError(
- "decode() does not allow star imports in the module code"
- )
-
- return value
-
- @classmethod
- def _create_model_from_callable_type(
- cls, typ: type[collections.abc.Callable]
- ) -> type[typing.Self]:
- """Create a SynthesizedFunction subclass carrying the requested signature in
- the model-facing description.
-
- Uses ``pydantic.create_model`` so the rendered signature (and any
- subclass-specific instructions) ride in the JSON schema ``description`` sent
- to the model. Subclasses customize the receiver rendering via `_param_names`
- and add guidance via `_extra_instructions`.
- """
- doc = (
- f"Python function with signature "
- f"{cls._signature_str(typ)}"
- f"{cls._extra_instructions()}"
- )
- return pydantic.create_model(
- "TypedSynthesizedFunction",
- __base__=cls,
- __doc__=doc,
- )
-
- @classmethod
- def _signature_str(cls, typ: type[collections.abc.Callable]) -> str:
- """Render a ``Callable[[...], ...]`` signature by type *name* (not its
- fully-qualified ``repr``), so the model sees ``Callable[[State], int]`` rather
- than ``collections.abc.Callable[[pkg.mod.State], builtins.int]``."""
- args = typing.get_args(typ)
- if not args:
- return "Callable"
- param_types, return_type = args
- params_str = (
- "..." if param_types is ... else ", ".join(cls._param_names(param_types))
- )
- return_str = getattr(return_type, "__name__", str(return_type))
- return f"Callable[[{params_str}], {return_str}]"
-
- @classmethod
- def _param_names(cls, param_types: typing.Iterable[typing.Any]) -> list[str]:
- return [getattr(t, "__name__", str(t)) for t in param_types]
-
- @classmethod
- def _extra_instructions(cls) -> str:
- return ""
+ return value
@TypeToPydanticType.register(collections.abc.Callable)
def _pydantic_callable(ty: typing.Any) -> typing.Any:
"""Pydantic-compatible Annotated type for a parameterized `Callable` value.
- The model *produces* a function (as ``code``); it is synthesized,
+ The model *produces* a function, as a string of its source; it is synthesized,
type-checked in the enclosing Skill's scope, and its own doctests are run.
Skill-body synthesis (`write_and_run_body`) has its own encoding,
`_pydantic_skill_body`.
"""
- typed_enc = SynthesizedFunction._create_model_from_callable_type(
- collections.abc.Callable[..., typing.Any] if not typing.get_args(ty) else ty # type: ignore[arg-type]
+ schema = _synthesized_source_schema(
+ collections.abc.Callable[..., typing.Any] if not typing.get_args(ty) else ty, # type: ignore[arg-type]
+ "the function",
+ _FUNCTION_CONSTRAINTS,
)
def _validate(
- value: SynthesizedFunction | dict | str | collections.abc.Callable,
+ value: str | collections.abc.Callable,
info: pydantic.ValidationInfo,
) -> collections.abc.Callable:
- if isinstance(value, str):
- value = typed_enc.model_validate({"code": value})
- elif isinstance(value, dict):
- value = typed_enc.model_validate(value)
- elif isinstance(value, EncodedFunction):
- value = typed_enc.model_validate(value.model_dump())
- elif callable(value):
+ value = _checked_source(value, require_annotations=True)
+ if not isinstance(value, str):
return value
- assert isinstance(value, typed_enc)
-
ctx = info.context or {}
anchor = ctx.get(_TYPE_CHECK_ANCHOR_KEY)
filename = f""
module: ast.Module = effectful.handlers.llm.harness.execution.hooks.parse(
- value.code, filename
+ value, filename
)
if anchor is not None and _recover_skill_def(anchor) is not None:
@@ -388,12 +381,11 @@ def _validate(
return result
# Distinct schemas per direction: validation (the model *produces* a function)
- # carries the synthesis instructions; serialization (the model *reads* an
- # encoded function) shows only the `code` shape `_serialize_synthesized`
- # emits, with no synthesis prose.
+ # carries the synthesis instructions; serialization (the model *reads* one) is
+ # bare source, with no synthesis prose.
return typing.Annotated[
ty,
pydantic.InstanceOf,
- pydantic.BeforeValidator(_validate, json_schema_input_type=typed_enc),
+ pydantic.BeforeValidator(_validate, json_schema_input_type=schema),
pydantic.PlainSerializer(_serialize_callable, return_type=EncodedFunction),
]
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_adder_function.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_adder_function.json
index 6fcbbb40a..526caadba 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_adder_function.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_adder_function.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def add_two_integers(a: int, b: int) -> int:\\n return a + b\"}}",
+ "content": "{\"value\":\"def add_two_integers(a: int, b: int) -> int:\\n return a + b\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_bool_return_type.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_bool_return_type.json
index 82eacecc4..9761acdd9 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_bool_return_type.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_bool_return_type.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def is_even(number: int) -> bool:\\n return number % 2 == 0\"}}",
+ "content": "{\"value\":\"def is_even(number: int) -> bool:\\n return number % 2 == 0\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter.json
index 91f05af9d..b2e4be71e 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def count_a_occurrences(input_string: str) -> int:\\n return input_string.count('a')\\n\"}}",
+ "content": "{\"value\":\"def count_a_occurrences(input_string: str) -> int:\\n return input_string.count('a')\\n\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_1.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_1.json
index d4f15f1fe..9c4b8d571 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_1.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_1.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"code\":\"def count_character_a(input_string: str) -> int:\\n return input_string.count('a')\\n\"}",
+ "content": "\"def count_character_a(input_string: str) -> int:\\n return input_string.count('a')\\n\"",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -41,4 +41,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_2.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_2.json
index d341dc68b..4794fdda8 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_2.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_counter_with_parameter_2.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"code\":\"def count_character_a(input_string: str) -> int:\\n return input_string.count('a')\\n\"}",
+ "content": "\"def count_character_a(input_string: str) -> int:\\n return input_string.count('a')\\n\"",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -41,4 +41,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_string_processor.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_string_processor.json
index 90ad3ed7d..d4a8e9805 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_string_processor.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_string_processor.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def transform_string(input_str: str) -> str:\\n transformed_str = input_str.upper() + '!!!'\\n return transformed_str\"}}",
+ "content": "{\"value\":\"def transform_string(input_str: str) -> str:\\n transformed_str = input_str.upper() + '!!!'\\n return transformed_str\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_three_params.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_three_params.json
index 562b1e82e..b303cafbd 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_three_params.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_three_params.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"from typing import Callable\\n\\ndef multiply_three_numbers(a: int, b: int, c: int) -> int:\\n return a * b * c\"}}",
+ "content": "{\"value\":\"from typing import Callable\\n\\ndef multiply_three_numbers(a: int, b: int, c: int) -> int:\\n return a * b * c\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method.json
index 587ede7c0..94ef69d56 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def add(a: int, b: int) -> int:\\n return a + b\\n\\nadd\"}}",
+ "content": "{\"value\":\"def add(a: int, b: int) -> int:\\n return a + b\\n\\nadd\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_1.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_1.json
index 3c944447e..ceef0ac28 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_1.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_1.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def add(a: int, b: int) -> int:\\n return a + b\\n\\n# The function to add two integers together.\\n\\nadd\"}}",
+ "content": "{\"value\":\"def add(a: int, b: int) -> int:\\n return a + b\\n\\n# The function to add two integers together.\\n\\nadd\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_2.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_2.json
index 914ef2740..1247a34bb 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_2.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_2.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def add(a: int, b: int) -> int:\\n return a + b\\n\\nadd\"}}",
+ "content": "{\"value\":\"def add(a: int, b: int) -> int:\\n return a + b\\n\\nadd\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_3.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_3.json
index cb02bb7bd..9087b9235 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_3.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesize_via_bound_method_3.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def add(a: int, b: int) -> int:\\n return a + b\"} }",
+ "content": "{\"value\":\"def add(a: int, b: int) -> int:\\n return a + b\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesized_function_roundtrip.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesized_function_roundtrip.json
index 4d09ca780..3abcb603c 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesized_function_roundtrip.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestCallableSynthesis__test_synthesized_function_roundtrip.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def add_two_integers(a: int, b: int) -> int:\\n return a + b\"}}",
+ "content": "{\"value\":\"def add_two_integers(a: int, b: int) -> int:\\n return a + b\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_1.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_1.json
index 6ab95f7ed..2f58f4480 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_1.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_1.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"from typing import Callable\\n\\ndef generate_example_paragraph() -> str:\\n return (\\n \\\"The cat decided to go for a walk. \\\"\\n \\\"Suddenly, it tripped and went tumbling. \\\"\\n \\\"The owner thought about getting another. \\\"\\n \\\"Everyone called him a lunatic.\\\"\\n )\"}}",
+ "content": "{\"value\":\"from typing import Callable\\n\\ndef generate_example_paragraph() -> str:\\n return (\\n \\\"The cat decided to go for a walk. \\\"\\n \\\"Suddenly, it tripped and went tumbling. \\\"\\n \\\"The owner thought about getting another. \\\"\\n \\\"Everyone called him a lunatic.\\\"\\n )\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_2.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_2.json
index 498a3e0b9..adba4f27f 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_2.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_2.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"from typing import Callable\\n\\ndef generate_example_paragraph() -> str:\\n return (\\n \\\"The cat decided to go for a walk. \\\"\\n \\\"Suddenly, it tripped and went tumbling. \\\"\\n \\\"The owner thought about getting another. \\\"\\n \\\"Everyone called him a lunatic.\\\"\\n )\"}}",
+ "content": "{\"value\":\"from typing import Callable\\n\\ndef generate_example_paragraph() -> str:\\n return (\\n \\\"The cat decided to go for a walk. \\\"\\n \\\"Suddenly, it tripped and went tumbling. \\\"\\n \\\"The owner thought about getting another. \\\"\\n \\\"Everyone called him a lunatic.\\\"\\n )\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_3.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_3.json
index 3bd8bb0ba..f36802187 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_3.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_3.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"from typing import Callable\\n\\ndef generate_example_paragraph() -> str:\\n return (\\n \\\"The cat decided to go for a walk. \\\"\\n \\\"Suddenly, it tripped and went tumbling. \\\"\\n \\\"The owner thought about getting another. \\\"\\n \\\"Everyone called him a lunatic.\\\"\\n )\"}}",
+ "content": "{\"value\":\"from typing import Callable\\n\\ndef generate_example_paragraph() -> str:\\n return (\\n \\\"The cat decided to go for a walk. \\\"\\n \\\"Suddenly, it tripped and went tumbling. \\\"\\n \\\"The owner thought about getting another. \\\"\\n \\\"Everyone called him a lunatic.\\\"\\n )\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_5.json b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_5.json
index ebbcec030..7966e201c 100644
--- a/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_5.json
+++ b/tests/fixtures/tests_test_handlers_llm_harness_provision.py__TestTenacityRetryer__test_codeadapt_notebook_replay_fixture_5.json
@@ -9,7 +9,7 @@
"finish_reason": "stop",
"index": 0,
"message": {
- "content": "{\"value\":{\"code\":\"def generate_paragraph() -> str:\\n return 'The sun rose gently over the horizon as Emily decided to take a morning walk. Along the path, a small squirrel caught her attention as it practiced its art of tumbling. Further down, she crossed paths with an old friend, reminding her that life often brings the joy of rediscovery in one form or another. As she returned home, the playful antics of the neighborhood cat reminded her of the exuberance of a young lunatic.'\"}}",
+ "content": "{\"value\":\"def generate_paragraph() -> str:\\n return 'The sun rose gently over the horizon as Emily decided to take a morning walk. Along the path, a small squirrel caught her attention as it practiced its art of tumbling. Further down, she crossed paths with an old friend, reminding her that life often brings the joy of rediscovery in one form or another. As she returned home, the playful antics of the neighborhood cat reminded her of the exuberance of a young lunatic.'\"}",
"role": "assistant",
"tool_calls": null,
"function_call": null,
@@ -43,4 +43,4 @@
}
},
"service_tier": "default"
-}
\ No newline at end of file
+}
diff --git a/tests/test_handlers_llm_harness_execution.py b/tests/test_handlers_llm_harness_execution.py
index 9a0b31408..b2130ef6a 100644
--- a/tests/test_handlers_llm_harness_execution.py
+++ b/tests/test_handlers_llm_harness_execution.py
@@ -32,7 +32,6 @@
from effectful.handlers.llm.harness.provision.litellm import LiteLLMConfigurer
from effectful.handlers.llm.harness.serialization import _TYPE_CHECK_ANCHOR_KEY
from effectful.handlers.llm.harness.synthesis.function import (
- SynthesizedFunction,
_recover_skill_def,
_splice_function,
)
@@ -318,9 +317,7 @@ class MyModel(pydantic.BaseModel):
fn = pydantic.TypeAdapter(
Encodable[Callable[[MyModel], MyModel]]
).validate_python(
- SynthesizedFunction(
- code="def identity(a: MyModel) -> MyModel:\n return a"
- ),
+ "def identity(a: MyModel) -> MyModel:\n return a",
context={"MyModel": MyModel},
)
assert fn(MyModel(x=1, y="hi")) == MyModel(x=1, y="hi")
@@ -363,9 +360,7 @@ def test_decode_with_anchor_typechecks_and_runs():
with handler(TYPE_CHECKER()), handler(BuiltinExecutor()):
ta = pydantic.TypeAdapter(Encodable[Callable[[str], int]])
fn = ta.validate_python(
- SynthesizedFunction(
- code="def count_a(s: str) -> int:\n return s.count('a')"
- ),
+ "def count_a(s: str) -> int:\n return s.count('a')",
context={_TYPE_CHECK_ANCHOR_KEY: _count_char},
)
assert fn("banana") == 3
@@ -376,7 +371,7 @@ def test_decode_with_anchor_rejects_bad_code():
ta = pydantic.TypeAdapter(Encodable[Callable[[str], int]])
with pytest.raises(Exception):
ta.validate_python(
- SynthesizedFunction(code="def count_a(s: str) -> str:\n return s"),
+ "def count_a(s: str) -> str:\n return s",
context={_TYPE_CHECK_ANCHOR_KEY: _count_char},
)
@@ -387,7 +382,7 @@ def test_decode_without_anchor_skips_typecheck():
with handler(TYPE_CHECKER()), handler(BuiltinExecutor()):
ta = pydantic.TypeAdapter(Encodable[Callable[[str], int]])
fn = ta.validate_python(
- SynthesizedFunction(code="def count_a(s: str) -> str:\n return s"),
+ "def count_a(s: str) -> str:\n return s",
context={},
)
assert callable(fn)
@@ -401,21 +396,21 @@ def test_decode_with_anchor_rejects_non_nestable():
ta = pydantic.TypeAdapter(Encodable[Callable[[str], int]])
with pytest.raises(Exception):
ta.validate_python(
- SynthesizedFunction(
- code="from os import *\ndef count_a(s: str) -> int:\n return 0"
- ),
+ "from os import *\ndef count_a(s: str) -> int:\n return 0",
context={_TYPE_CHECK_ANCHOR_KEY: _count_char},
)
def test_decode_without_anchor_still_rejects_non_nestable():
- # The splice-time scan is anchor-conditional, but `SynthesizedFunction` states the
- # no-star-import rule to the model as an unconditional constraint on `code`
- # -- so it is enforced unconditionally too, and the anchorless path rejects it just
- # as the spliced one does (here at model validation, before any provider runs).
+ # The splice-time scan is anchor-conditional, but the no-star-import rule is
+ # stated to the model as an unconditional constraint on the source -- so it is
+ # enforced unconditionally too, and the anchorless path rejects it just as the
+ # spliced one does (here before any provider runs).
+ ta = pydantic.TypeAdapter(Encodable[Callable[[str], int]])
with pytest.raises(pydantic.ValidationError):
- SynthesizedFunction(
- code="from os import *\ndef count_a(s: str) -> int:\n return 0"
+ ta.validate_python(
+ "from os import *\ndef count_a(s: str) -> int:\n return 0",
+ context={},
)
@@ -426,10 +421,8 @@ def test_decode_without_anchor_still_rejects_non_nestable():
def test_restricted_blocks_private_attribute_access():
"""RestrictedPython blocks access to underscore-prefixed attributes by default."""
- source = SynthesizedFunction(
- code="""def get_private(s: str) -> int:
+ source = """def get_private(s: str) -> int:
return s.__class__.__name__"""
- )
# Should raise due to restricted attribute access
with pytest.raises(Exception): # Could be NameError or AttributeError
with handler(TYPE_CHECKER()), handler(RestrictedPythonExecutor()):
@@ -446,10 +439,8 @@ def test_restricted_with_custom_policy():
class CustomPolicy(RestrictingNodeTransformer):
pass
- source = SynthesizedFunction(
- code="""def add(a: int, b: int) -> int:
+ source = """def add(a: int, b: int) -> int:
return a + b"""
- )
with (
handler(TYPE_CHECKER()),
handler(RestrictedPythonExecutor(policy=CustomPolicy)),
@@ -473,10 +464,8 @@ def test_builtins_in_env_does_not_bypass_security():
dangerous_ctx = {"__builtins__": builtins.__dict__}
# Test 1: open() should not be usable even with __builtins__ in context
- source_open = SynthesizedFunction(
- code="""def read_file(path: str) -> str:
+ source_open = """def read_file(path: str) -> str:
return open(path).read()"""
- )
with pytest.raises(Exception): # Could be NameError, ValueError, or other
with handler(TYPE_CHECKER()), handler(RestrictedPythonExecutor()):
fn = pydantic.TypeAdapter(Encodable[Callable[[str], str]]).validate_python(
@@ -485,11 +474,9 @@ def test_builtins_in_env_does_not_bypass_security():
fn("/etc/passwd")
# Test 2: __import__ should not be usable
- source_import = SynthesizedFunction(
- code="""def get_os_name() -> str:
+ source_import = """def get_os_name() -> str:
os = __import__('os')
return os.name"""
- )
with pytest.raises(Exception):
with handler(TYPE_CHECKER()), handler(RestrictedPythonExecutor()):
fn = pydantic.TypeAdapter(Encodable[Callable[[], str]]).validate_python(
@@ -498,10 +485,8 @@ def test_builtins_in_env_does_not_bypass_security():
fn()
# Test 3: Verify safe code still works with dangerous context
- source_safe = SynthesizedFunction(
- code="""def add(a: int, b: int) -> int:
+ source_safe = """def add(a: int, b: int) -> int:
return a + b"""
- )
with handler(TYPE_CHECKER()), handler(RestrictedPythonExecutor()):
fn = pydantic.TypeAdapter(Encodable[Callable[[int, int], int]]).validate_python(
source_safe, context=dangerous_ctx
@@ -509,10 +494,8 @@ def test_builtins_in_env_does_not_bypass_security():
assert fn(2, 3) == 5, "Safe code should still work"
# Test 4: Private attribute access should still be blocked
- source_private = SynthesizedFunction(
- code="""def get_class(s: str) -> str:
+ source_private = """def get_class(s: str) -> str:
return s.__class__.__name__"""
- )
with pytest.raises(Exception):
with handler(TYPE_CHECKER()), handler(RestrictedPythonExecutor()):
fn = pydantic.TypeAdapter(Encodable[Callable[[str], str]]).validate_python(
@@ -1237,7 +1220,7 @@ def _decode(self, code: str, provider=None):
with handler(TYPE_CHECKER()), handler(provider):
return pydantic.TypeAdapter(
Encodable[Callable[[str, str], int]]
- ).validate_python(SynthesizedFunction(code=code), context={})
+ ).validate_python(code, context={})
def test_decode_runs_passing_doctests(self):
fn = self._decode(
diff --git a/tests/test_handlers_llm_harness_provision.py b/tests/test_handlers_llm_harness_provision.py
index 16e927027..f10ef9494 100644
--- a/tests/test_handlers_llm_harness_provision.py
+++ b/tests/test_handlers_llm_harness_provision.py
@@ -51,6 +51,7 @@
)
from effectful.handlers.llm.harness.observability.rich import RichTerminalRenderer
from effectful.handlers.llm.harness.provision.litellm import LiteLLMConfigurer
+from effectful.handlers.llm.harness.serialization import _NameAndTool
from effectful.handlers.llm.harness.synthesis.body import (
FinalBodySynthesizer,
)
@@ -1545,11 +1546,11 @@ def test_synthesized_function_roundtrip(self, request):
add_func = synthesize_adder()
assert callable(add_func)
- # Encode it back to SynthesizedFunction
+ # Encode it back to source
adapter = pydantic.TypeAdapter(Encodable[Callable[[int, int], int]])
encoded = adapter.dump_python(add_func, mode="json")
- assert isinstance(encoded, dict)
- assert "def " in encoded["code"]
+ assert isinstance(encoded, str)
+ assert "def " in encoded
# Decode it again and verify it still works
decoded = adapter.validate_python(encoded)
@@ -1611,7 +1612,7 @@ def make_write_and_run_body_response(
synthesis ``write_and_run_body`` tool with a function it wrote."""
return make_tool_call_response(
"write_and_run_body",
- json.dumps({"implementation": {"code": code}, "compact": compact}),
+ json.dumps({"implementation": code, "compact": compact}),
tool_call_id=tool_call_id,
)
@@ -1890,9 +1891,7 @@ def test_write_and_run_body_mixed_with_normal_call_is_rejected(self):
"name": "write_and_run_body",
"arguments": json.dumps(
{
- "implementation": {
- "code": "def double_it(x: int) -> int:\n return x * 2\n"
- }
+ "implementation": "def double_it(x: int) -> int:\n return x * 2\n"
}
),
},
@@ -1938,6 +1937,27 @@ def variadic(*args: int) -> int:
variadic, variadic.__signature__.bind()
)
+ def test_implementation_is_advertised_as_a_bare_string(self):
+ """The tool the model actually sees takes source as a JSON string, with no
+ object to assemble and no `$ref` to resolve (#775)."""
+
+ @Skill.define
+ def add(a: int, b: int) -> int:
+ """Add {a} and {b}."""
+ raise NotHandled
+
+ tool = FinalBodySynthesizer._SubmitSolutionTool.define(
+ add, add.__signature__.bind(1, 2)
+ )
+ advertised = pydantic.TypeAdapter(Encodable[_NameAndTool]).dump_python(
+ _NameAndTool("write_and_run_body", tool), mode="json", context={}
+ )
+ implementation = advertised["function"]["parameters"]["properties"][
+ "implementation"
+ ]
+ assert implementation["type"] == "string"
+ assert "$ref" not in json.dumps(implementation)
+
class TestSynthesizeAndCallDoctests:
"""SynthesizeAndCall validates the synthesized function against the
diff --git a/tests/test_handlers_llm_harness_serialization.py b/tests/test_handlers_llm_harness_serialization.py
index 940bd5f82..dcb10d6b0 100644
--- a/tests/test_handlers_llm_harness_serialization.py
+++ b/tests/test_handlers_llm_harness_serialization.py
@@ -42,6 +42,7 @@
_is_decodable,
_NameAndTool,
_UndecodableReturn,
+ format_as_content_blocks,
to_content_blocks,
)
from effectful.handlers.llm.harness.validation.ty import TyTypeChecker
@@ -848,7 +849,7 @@ def test_encodable_callable_produces_valid_schema_631():
adapter = pydantic.TypeAdapter(Encodable[Callable[[int], int]])
schema = adapter.json_schema()
assert isinstance(schema, dict)
- assert "properties" in schema
+ assert schema["type"] == "string"
def test_dataclass_with_encodable_tuple_field_626():
@@ -952,8 +953,8 @@ def test_recursive_alias_does_not_diverge(ty):
@pytest.mark.xfail(
strict=True,
reason="The alias now reaches the type-expression encoding and gets a real "
- "schema, but `Kernel` aliases a *callable*, whose schema is `EncodedFunction` "
- "-- correct, and signature-free, as every callable schema here is. So the "
+ "schema, but `Kernel` aliases a *callable*, whose schema is a bare source "
+ "string -- correct, and signature-free, as every callable schema here is. So the "
"encoding says 'a function' without saying `list[float]`. An alias to any "
"other shape (see the tuple case) comes out fully described.",
)
@@ -1054,7 +1055,7 @@ def test_class_value_is_not_encoded_as_a_function():
encoded = pydantic.TypeAdapter(Encodable[typ]).dump_python(
int, mode="json", context={}
)
- assert "code" not in encoded
+ assert isinstance(encoded, dict), "a type encodes as a schema, not as source"
assert "def int" not in json.dumps(encoded)
@@ -1072,7 +1073,7 @@ def test_dataclass_value_routes_to_the_callable_encoding():
encoded = pydantic.TypeAdapter(Encodable[typ]).dump_python(
_Point, mode="json", context={}
)
- assert "class _Point" in encoded["code"]
+ assert "class _Point" in encoded
@pytest.mark.parametrize("value", [int, list[int], int | str], ids=str)
@@ -1390,16 +1391,11 @@ def _int_pair_anchor() -> Callable[[int, int], int]:
# Callable error cases: (type, ctx, source, exc_type, anchor)
#
-# Sources are passed as raw ``{"code": ...}`` dicts, not pre-built
-# ``SynthesizedFunction`` instances: structurally-invalid code (e.g. a non-function
-# last statement) is rejected by ``SynthesizedFunction``'s own field validator, so
-# building it eagerly here would raise at collection. A dict defers that validation
-# to the decoder (``model_validate``), which is the real path an LLM's JSON takes.
CALLABLE_ERROR_CASES = [
pytest.param(
Callable[..., int],
{},
- {"code": "x = 42"},
+ "x = 42",
ValueError,
None,
id="non-function-last-stmt",
@@ -1407,7 +1403,7 @@ def _int_pair_anchor() -> Callable[[int, int], int]:
pytest.param(
Callable[[int, int], int],
{},
- {"code": "def add(a: int) -> int:\n return a"},
+ "def add(a: int) -> int:\n return a",
ValueError,
None,
id="wrong-param-count",
@@ -1415,7 +1411,7 @@ def _int_pair_anchor() -> Callable[[int, int], int]:
pytest.param(
Callable[[int, int], int],
{},
- {"code": "def add(a: int, b: int) -> str:\n return str(a + b)"},
+ "def add(a: int, b: int) -> str:\n return str(a + b)",
TypeError,
_int_pair_anchor,
id="wrong-return-type",
@@ -1423,7 +1419,7 @@ def _int_pair_anchor() -> Callable[[int, int], int]:
pytest.param(
Callable[[int, int], int],
{},
- {"code": "def add(a: int, b: int):\n return a + b"},
+ "def add(a: int, b: int):\n return a + b",
ValueError,
None,
id="missing-return-annotation",
@@ -1660,11 +1656,50 @@ def test_encodable_code_schema_is_a_string():
assert schema["type"] == "string"
+# ============================================================================
+# A synthesized callable is a bare string on the wire, in both directions (#775)
+# ============================================================================
+
+
+@pytest.mark.parametrize("mode", ["validation", "serialization"])
+def test_callable_schema_is_a_bare_string_with_no_refs(mode):
+ """A function is a JSON string, not an object wrapping one.
+
+ The absence of `$ref`/`$defs` is the point: a provider that decodes structured
+ output approximately has no object to assemble and no escaping to get right.
+ """
+ schema = pydantic.TypeAdapter(Encodable[Callable[[int, int], int]]).json_schema(
+ mode=mode
+ )
+ assert schema["type"] == "string"
+ assert "$ref" not in json.dumps(schema)
+ assert "$defs" not in schema
+
+
+def test_callable_round_trips_through_its_string_encoding():
+ """Serializing a function and validating the result back yields a working one."""
+ adapter = pydantic.TypeAdapter(Encodable[Callable[[int, int], int]])
+ encoded = adapter.dump_python(fn_add, mode="json", context={})
+ assert isinstance(encoded, str)
+ with handler(TyTypeChecker()), handler(BuiltinExecutor()):
+ decoded = adapter.validate_python(encoded, context={})
+ assert decoded(2, 3) == fn_add(2, 3)
+
+
+def test_callable_in_a_prompt_arrives_as_unescaped_source():
+ """A function spliced into a prompt reaches the model as source, not as source
+ escaped inside a JSON object."""
+ blocks = format_as_content_blocks("{fn}", {"fn": fn_add})
+ text = "".join(b["text"] for b in blocks)
+ assert text.strip().startswith("def fn_add")
+ assert "\\n" not in text
+
+
# ============================================================================
# Serializing a callable: `Encodable[Callable]`'s two directions carry different
# obligations
#
-# Validation decodes code the model *wrote*, and holds it to `SynthesizedFunction`'s
+# Validation decodes code the model *wrote*, and holds it to the synthesis
# constraints. Serialization encodes a value that already exists, which was never
# under those constraints -- a class read out of the lexical scope, or an inner
# function a Skill *body* returned. Conflating the two aborted the enclosing call
@@ -1700,19 +1735,20 @@ def test_serialize_callable_does_not_reapply_synthesis_constraints(label, value)
encoded = pydantic.TypeAdapter(Encodable[Callable[[int, int], int]]).dump_python(
value, mode="json", context={}
)
- assert "code" in encoded, label
- assert encoded["code"].strip(), label
+ assert isinstance(encoded, str), label
+ assert encoded.strip(), label
def test_serialize_callable_matches_its_declared_schema():
- """What serialization emits is what its JSON schema promises: the plain
- `EncodedFunction` shape, with none of the synthesis constraints attached."""
+ """What serialization emits is what its JSON schema promises: a bare source
+ string, with none of the synthesis constraints attached."""
adapter = pydantic.TypeAdapter(Encodable[Callable[[int, int], int]])
schema = adapter.json_schema(mode="serialization")
encoded = adapter.dump_python(
_outer_returning_unannotated(), mode="json", context={}
)
- assert set(encoded) <= set(schema["properties"])
+ assert schema["type"] == "string"
+ assert isinstance(encoded, str)
@pytest.mark.parametrize(
@@ -1728,4 +1764,4 @@ def test_serialize_tool_value_encodes_the_callable_it_is(ty):
encoded = pydantic.TypeAdapter(Encodable[ty]).dump_python(
_tool_add, mode="json", context={}
)
- assert "def _tool_add" in encoded["code"]
+ assert "def _tool_add" in encoded
diff --git a/tests/test_handlers_llm_harness_toolcall.py b/tests/test_handlers_llm_harness_toolcall.py
index ea9dc3957..3c32eae88 100644
--- a/tests/test_handlers_llm_harness_toolcall.py
+++ b/tests/test_handlers_llm_harness_toolcall.py
@@ -907,9 +907,7 @@ def test_generic_callable_skill_parametric_impl(generic_mod):
lambda: generic_mod.make_fn(int),
[
make_text_response(
- json.dumps(
- {"value": {"code": "def ident[U](x: U) -> U:\n return x\n"}}
- )
+ json.dumps({"value": "def ident[U](x: U) -> U:\n return x\n"})
)
],
)
@@ -931,13 +929,7 @@ def test_generic_callable_skill_concrete_impl_rejected(generic_mod):
lambda: generic_mod.make_fn(int),
[
make_text_response(
- json.dumps(
- {
- "value": {
- "code": "def dbl(x: int) -> int:\n return x + x\n"
- }
- }
- )
+ json.dumps({"value": "def dbl(x: int) -> int:\n return x + x\n"})
)
],
)