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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .semgrep.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2206,6 +2206,36 @@ rules:
category: doexpr-hierarchy
rationale: "return_clause was removed; post-process handler results via ordinary @do composition"

- id: doeff-no-public-withhandler-shim
pattern-either:
- pattern-regex: (?m)^\s*from\s+doeff\s+import\s+.*\bWithHandler\b
- pattern-regex: (?m)^\s*from\s+doeff\.program\s+import\s+.*\bWithHandler\b
- pattern-regex: (?m)^\s*\(import\s+doeff\s+\[[^\]]*\bWithHandler\b
- pattern-regex: (?m)^\s*\(import\s+doeff\.program\s+\[[^\]]*\bWithHandler\b
- pattern-regex: \bdoeff\.WithHandler\s*\(
- pattern-regex: \bprogram\.WithHandler\s*\(
paths:
include:
- "doeff/**/*.py"
- "packages/**/*.py"
- "packages/**/*.pyi"
- "packages/**/*.hy"
- "tests/**/*.py"
- "tests/**/*.hy"
- "examples/**/*.py"
- "examples/**/*.hy"
exclude:
- "tests/semgrep/fixtures/**"
message: |
Public WithHandler shim usage is banned (core-withhandler-compat-shim-delete).
Compose handlers by calling Program -> Program handlers directly: handler(program).
Deliberate VM-node tests must import WithHandler from doeff_vm explicitly.
languages: [generic]
severity: ERROR
metadata:
category: doexpr-hierarchy
rationale: "public WithHandler compatibility shim was deleted; h(body) is the only user idiom"

- id: no-legacy-intercept-api
pattern-either:
- pattern-regex: (?m)^from\s+doeff\s+import\s+.*\bIntercept\b
Expand Down
4 changes: 3 additions & 1 deletion benchmarks/benchmark_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@
from doeff_vm import EffectBase

from doeff import Apply, Pass, Pure, Resume, do, run
from doeff.program import WithHandlerType as VMWithHandler
from doeff.program import (
WithHandlerType as VMWithHandler,
)

DEFAULT_RUNS = 20
DEFAULT_LOOP_ITERATIONS = 100
Expand Down
8 changes: 5 additions & 3 deletions doeff/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@
from doeff.program import ResumeThrow as ResumeThrow
from doeff.program import Transfer as Transfer
from doeff.program import TransferThrow as TransferThrow
from doeff.program import WithHandler as WithHandler
from doeff.program import WithHandlerType as WithHandlerType
from doeff.program import (
WithHandlerType as WithHandlerType,
)
from doeff.program import WithObserve as WithObserveRaw
from doeff.program import handler as handler
from doeff.program import program as program
from doeff.result import Err as Err
from doeff.result import Maybe as Maybe
Expand Down Expand Up @@ -116,7 +118,7 @@ class DoExpr(metaclass=_DoExprMeta):
"""Virtual base type for all doeff program nodes.

isinstance(x, DoExpr) returns True for any program node
(Pure, Expand, WithHandler, etc.).
(Pure, Expand, WithHandlerType, etc.).
"""


Expand Down
6 changes: 4 additions & 2 deletions doeff/cli/run_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pathlib import Path
from typing import Any

from doeff.program import handler as _program_handler

# Activate Hy's import hook so doeff run can resolve program/interpreter/env
# paths that live in .hy files (e.g. nakagawa.live.agent_pipeline.p_agent_daily).
# Without this, importlib.import_module for a .hy module silently creates an
Expand Down Expand Up @@ -183,15 +185,15 @@ def default_interpreter(program: Any) -> Any:
)
from doeff_core_effects.scheduler import scheduled

from doeff import WithHandler, run
from doeff import run

handlers = [
lazy_ask(), state(), writer(), try_handler, slog_handler(),
listen_handler, await_handler(),
]
wrapped = program
for h in reversed(handlers):
wrapped = WithHandler(h, wrapped)
wrapped = _program_handler(h)(wrapped)
return run(scheduled(wrapped))


Expand Down
2 changes: 1 addition & 1 deletion doeff/handler_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_inner_handlers(k):
Usage in a handler:
inner_hs = yield get_inner_handlers(k)
for h in inner_hs:
prog = WithHandler(h, prog)
prog = handler(h)(prog)
"""
all_hs = yield GetHandlers(k)
if all_hs:
Expand Down
76 changes: 27 additions & 49 deletions doeff/program.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
The VM classifies them via downcast (not tag-based getattr).
"""

from typing import Any, cast

from doeff_vm import Apply as Apply
from doeff_vm import Expand as Expand
from doeff_vm import GetExecutionContext as GetExecutionContext
Expand All @@ -17,58 +19,34 @@
from doeff_vm import ResumeThrow as ResumeThrow
from doeff_vm import Transfer as Transfer
from doeff_vm import TransferThrow as TransferThrow
from doeff_vm import WithHandler as _WithHandlerNode
from doeff_vm import WithHandler as WithHandlerType
from doeff_vm import WithObserve as WithObserve

WithHandlerType = _WithHandlerNode


_NEW_STYLE_DEPRECATION_MSG = (
"WithHandler(h, body) is deprecated: h is already a Program -> Program "
"function produced by defhandler, call it directly as h(body) "
"(or in Hy: (h body)). The shim stays in place indefinitely for "
"backward compatibility but emits this warning to steer new code."
)

_LEGACY_DEPRECATION_MSG = (
"WithHandler(h, body) with a raw @do-dispatcher ``h`` is deprecated: "
"rewrite the handler with defhandler (Hy) or @handler-style factory "
"that returns a Program -> Program function, then call it as h(body). "
"The shim stays in place indefinitely for backward compatibility but "
"emits this warning to steer new code toward the PR A1 idiom."
)


def WithHandler(h, body, *args, **kwargs): # noqa: N802 - public compatibility constructor
"""Install handler ``h`` around ``body`` — **deprecated**.

New-style handlers built with ``defhandler`` are already Program →
Program functions; prefer calling them directly::

# Before
WithHandler(my_handler, program)
# After
my_handler(program)

Accepts two forms for backward compatibility:

- New-style: ``h`` is a function ``Program -> Program`` marked with
``_doeff_is_handler_fn = True``. The call is forwarded as ``h(body)``.
Emits :class:`DeprecationWarning`.
- Legacy: ``h`` is a raw ``@do``-decorated dispatcher ``fn[effect, k]``.
Falls through to the Rust ``WithHandler`` pyclass. Emits
:class:`DeprecationWarning` pointing at ``defhandler``.

The shim itself is permanent (scope A) — the warning is informational
and does not break existing code.
"""
import warnings

if getattr(h, "_doeff_is_handler_fn", False):
warnings.warn(_NEW_STYLE_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
return h(body, *args, **kwargs)
warnings.warn(_LEGACY_DEPRECATION_MSG, DeprecationWarning, stacklevel=2)
return _WithHandlerNode(h, body, *args, **kwargs)
def handler(raw_handler):
"""Wrap a raw effect dispatcher as a Program -> Program handler."""
if not callable(raw_handler):
raise TypeError(
f"handler: raw_handler must be callable, got {type(raw_handler).__name__}"
)
raw_handler_meta = cast(Any, raw_handler)
try:
is_handler_fn = raw_handler_meta._doeff_is_handler_fn
except AttributeError:
is_handler_fn = False
if is_handler_fn is True:
return raw_handler

def install(body):
return WithHandlerType(raw_handler, body)

install.__name__ = raw_handler_meta.__name__
install.__qualname__ = raw_handler_meta.__qualname__
install.__doc__ = raw_handler_meta.__doc__
install_meta = cast(Any, install)
install_meta._doeff_is_handler_fn = True
install_meta.__doeff_handler_data__ = raw_handler
return install


def program(gen_fn, *args):
Expand Down
10 changes: 2 additions & 8 deletions packages/doeff-agentic/examples/01_hello_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def hello_agent():
if __name__ == "__main__":
import asyncio

from doeff import WithHandler, async_run, default_handlers
from doeff import async_run, default_handlers

async def main():
print("Starting hello_agent workflow...")
Expand All @@ -58,13 +58,7 @@ async def main():
# Merge preset handlers with opencode handlers
# Preset provides: slog display (WriterTellEffect) + config (Ask preset.*)
# OpenCode provides: agent session management effects
program = WithHandler(
preset_handlers(),
WithHandler(
opencode_handler(),
hello_agent(),
),
)
program = preset_handlers()(opencode_handler()(hello_agent()))
result = await async_run(program, handlers=default_handlers())

if result.is_err():
Expand Down
10 changes: 2 additions & 8 deletions packages/doeff-agentic/examples/02_agent_with_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def agent_with_status():
if __name__ == "__main__":
import asyncio

from doeff import WithHandler, async_run, default_handlers
from doeff import async_run, default_handlers

async def main():
print("Starting agent_with_status workflow...")
Expand All @@ -75,13 +75,7 @@ async def main():
# Merge preset handlers with opencode handlers
# Preset provides: slog display (WriterTellEffect) + config (Ask preset.*)
# OpenCode provides: agent session management effects
program = WithHandler(
preset_handlers(),
WithHandler(
opencode_handler(),
agent_with_status(),
),
)
program = preset_handlers()(opencode_handler()(agent_with_status()))
result = await async_run(program, handlers=default_handlers())

if result.is_err():
Expand Down
10 changes: 2 additions & 8 deletions packages/doeff-agentic/examples/03_sequential_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def research_and_summarize(topic: str):
if __name__ == "__main__":
import asyncio

from doeff import WithHandler, async_run, default_handlers
from doeff import async_run, default_handlers

async def main():
topic = "functional programming"
Expand All @@ -79,13 +79,7 @@ async def main():
# Merge preset handlers with opencode handlers
# Preset provides: slog display (WriterTellEffect) + config (Ask preset.*)
# OpenCode provides: agent session management effects
program = WithHandler(
preset_handlers(),
WithHandler(
opencode_handler(),
research_and_summarize(topic),
),
)
program = preset_handlers()(opencode_handler()(research_and_summarize(topic)))
result = await async_run(program, handlers=default_handlers())

if result.is_err():
Expand Down
10 changes: 2 additions & 8 deletions packages/doeff-agentic/examples/04_conditional_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def review_and_maybe_fix(code: str):
if __name__ == "__main__":
import asyncio

from doeff import WithHandler, async_run, default_handlers
from doeff import async_run, default_handlers

async def main():
# Code with issues to review
Expand All @@ -101,13 +101,7 @@ def calculate_average(numbers):
# Merge preset handlers with opencode handlers
# Preset provides: slog display (WriterTellEffect) + config (Ask preset.*)
# OpenCode provides: agent session management effects
program = WithHandler(
preset_handlers(),
WithHandler(
opencode_handler(),
review_and_maybe_fix(sample_code),
),
)
program = preset_handlers()(opencode_handler()(review_and_maybe_fix(sample_code)))
result = await async_run(program, handlers=default_handlers())

if result.is_err():
Expand Down
10 changes: 2 additions & 8 deletions packages/doeff-agentic/examples/05_human_in_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ def draft_with_approval(task: str):
if __name__ == "__main__":
import asyncio

from doeff import WithHandler, async_run, default_handlers
from doeff import async_run, default_handlers

async def main():
task = "Write a haiku about programming"
Expand All @@ -176,13 +176,7 @@ async def main():
# Merge preset handlers with opencode handlers
# Preset provides: slog display (WriterTellEffect) + config (Ask preset.*)
# OpenCode provides: agent session management effects
program = WithHandler(
preset_handlers(),
WithHandler(
opencode_handler(),
draft_with_approval(task),
),
)
program = preset_handlers()(opencode_handler()(draft_with_approval(task)))
result = await async_run(program, handlers=default_handlers())

if result.is_err():
Expand Down
10 changes: 2 additions & 8 deletions packages/doeff-agentic/examples/06_parallel_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ def multi_perspective_analysis(topic: str):
if __name__ == "__main__":
import asyncio

from doeff import WithHandler, async_run, default_handlers
from doeff import async_run, default_handlers

async def main():
topic = "AI code assistants"
Expand All @@ -127,13 +127,7 @@ async def main():
# Merge preset handlers with opencode handlers
# Preset provides: slog display (WriterTellEffect) + config (Ask preset.*)
# OpenCode provides: agent session management effects
program = WithHandler(
preset_handlers(),
WithHandler(
opencode_handler(),
multi_perspective_analysis(topic),
),
)
program = preset_handlers()(opencode_handler()(multi_perspective_analysis(topic)))
result = await async_run(program, handlers=default_handlers())

if result.is_err():
Expand Down
10 changes: 2 additions & 8 deletions packages/doeff-agentic/examples/07_pr_review_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def pr_review_workflow(pr_url: str, require_approval: bool = False):
import asyncio
import sys

from doeff import WithHandler, async_run, default_handlers
from doeff import async_run, default_handlers

async def main():
# Use a sample PR URL or accept from command line
Expand All @@ -226,13 +226,7 @@ async def main():
# Merge preset handlers with opencode handlers
# Preset provides: slog display (WriterTellEffect) + config (Ask preset.*)
# OpenCode provides: agent session management effects
program = WithHandler(
preset_handlers(),
WithHandler(
opencode_handler(),
pr_review_workflow(pr_url, require_approval),
),
)
program = preset_handlers()(opencode_handler()(pr_review_workflow(pr_url, require_approval)))

try:
result = await async_run(program, handlers=default_handlers())
Expand Down
4 changes: 2 additions & 2 deletions packages/doeff-agentic/examples/08_testing_with_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)
from doeff_agentic.handlers.testing import MockAgenticHandler, mock_handlers

from doeff import WithHandler, default_handlers, do, run
from doeff import default_handlers, do, run


@do
Expand All @@ -42,7 +42,7 @@ def mock_conversation():
def main() -> int:
# Compose the mock protocol handler directly with WithHandler.
handler_impl = MockAgenticHandler(workflow_name="mock-example")
program = WithHandler(mock_handlers(handler_impl), mock_conversation())
program = mock_handlers(handler_impl)(mock_conversation())
result = run(program, handlers=default_handlers())

if result.is_err():
Expand Down
Loading
Loading