@agentic_function turns an ordinary Python function into an Agentic Function: each call is recorded as a code node in the session DAG, and the llm() calls inside the function body are recorded as llm nodes.
This document defines the decorator and authoring conventions for agentic functions.
from openprogram import agentic_function
from openprogram.agentic_programming import llm
@agentic_function
def f(x: str, runtime) -> str:
"""One-line summary of what f does."""
return llm([{"type": "text", "text": f"...{x}..."}])You can use bare @agentic_function or the parameterized form @agentic_function(...).
| Parameter | Type | Default | Description |
|---|---|---|---|
resumable |
bool |
False |
Opt in to explicit durable steps, JSON state, and function code selection after restart. See below. |
expose |
str |
"io" |
Outward-facing: what others can see about me when they render the DAG. "io" = only the function's name and return value are visible externally, while its internals (LLM exchanges, sub-calls) are hidden; "llm" = the reverse, exposing only the internal LLM exchanges and hiding the function's own name/return value and nested code sub-calls; "full" = everything visible (docstring + params + output + LLM replies + internals); "hidden" = no DAG nodes are written at all. Any other value raises ValueError at decoration time |
render_range |
dict |
None |
Inward-facing: how many history nodes to read from the DAG when this function's internal llm() call assembles its prompt. Shape {"callers": N, "subcalls": M}, where both numbers are node counts (sliced by seq):• callers — nodes written before this function's frame started; take the most recent N (None default = unlimited, 0 = a full wall)• subcalls — nodes already written after this function's frame started; take the most recent N (-1 default = unlimited, so the frame naturally sees its own progress; N>=0 = set explicitly when you want to truncate the prompt; 0 = wall off in-frame entirely){"callers":0,"subcalls":0} = cut off from both the outside world and your own frame |
input |
dict |
None |
Per-parameter UI metadata; the WebUI renders the input form from it. Supported fields per parameter: description (label next to the name), placeholder (example text), multiline (True = textarea), options (list of allowed values, rendered as a dropdown and emitted as a JSON-schema enum), hidden (True = exclude from the form and from the LLM tool schema) |
system |
str |
None |
The system prompt for this function's LLM calls (applied over the injected runtime for the duration of the call, then restored afterward) |
Every @agentic_function is also registered as an LLM-callable tool in the shared registry (openprogram.programs), alongside @function-decorated tools. These parameters control that registration and share their names and semantics with @function:
| Parameter | Type | Default | Description |
|---|---|---|---|
as_tool |
bool |
True |
Register this function as an LLM-callable tool. False = Python-direct-invoke only |
name |
str |
None |
Tool name override. Default: the function's __name__ |
description |
str |
None |
Tool description override. Default: the function's docstring |
parameters |
dict |
None |
JSON-schema parameter override. Default: auto-generated from the signature's type hints plus input metadata (runtime-injected and hidden parameters excluded) |
label |
str |
None |
Human-readable label shown in tool UIs |
toolset |
tuple |
() |
Toolset names this tool belongs to (used by exec(toolset=...) presets) |
unsafe_in |
tuple |
() |
Channel sources in which the tool is considered unsafe and filtered out |
check_fn |
Callable |
None |
Per-call gate: called before dispatch; a falsy result blocks the call |
requires_env |
tuple |
() |
Environment variable names that must be set for the tool to be offered |
can_use |
Callable |
None |
Dynamic availability predicate evaluated at tool-resolution time |
max_result_chars |
int |
None |
Truncation cap for the tool result fed back to the model. None = the registry default DEFAULT_MAX_RESULT_CHARS (30,000 chars) |
persist_full |
bool |
False |
Persist the untruncated result to disk so the agent can read it back |
head_ratio |
float |
None |
When truncating, the fraction kept from the head, rest from the tail. None = the registry default DEFAULT_HEAD_RATIO (0.7) |
requires_approval |
— | None |
Approval requirement forwarded to the tool registry (same shape as @function) |
cache |
bool |
False |
Memoize results on (name, args) for tool-dispatched calls |
cache_ttl |
float |
300.0 |
Cache lifetime in seconds when cache=True |
timeout |
float |
None |
Hard wall-clock kill for a tool-dispatched call, in seconds; on expiry the model receives an error result |
available_if |
Callable |
None |
Import-time gate: if it returns falsy (or raises), the decorator is skipped entirely and the module-level name stays a plain function — no wrapper, no registration |
defer |
bool |
False |
Register as a deferred tool (schema loaded on demand instead of shipped with every call) |
register_globally |
bool |
True |
False = build the tool but keep it out of the global registry |
The function name, parameter names / types / defaults, and the one-line summary are all read automatically from the function signature and docstring, not repeated in the decorator (see SKILL.md §3).
Parameters named runtime, exec_runtime, or review_runtime are auto-injected: if the caller passes none (or None), the runtime is taken from the current call chain, or — for an entry-point call — created via create_runtime() (auto-detection) and closed again when the function returns. A function may declare more than one runtime parameter; all of them are filled with the same runtime. These parameters never appear in the LLM tool schema or the WebUI form.
fn.spec— the auto-generated JSON-schema tool spec ({"name", "description", "parameters"});fn.execute(**kwargs)invokes the wrapper with LLM-provided kwargs.- Self-recursion backstop: a function that re-enters itself more than 5 levels deep raises
RecursionError(the model is also steered away from self-calls by an injected situational prompt). - Pre-invocation hooks (
add_pre_invocation_hook/remove_pre_invocation_hook) run at the top of every call and may raiseCancelledErrorto abort it (this is how the WebUI stop button works).
- Entering the function: write a
codenode (output=None,status="running"), and store the function docstring into that node'smetadata.doc, which is prepended tofunction_name(args)when rendering context. llm()inside the function body: each call writes anllmnode.- Exiting the function: backfill the same
codenode'soutput/status.
When expose="hidden", no nodes are written. In standalone runs (with no DAG store installed), all recording is a no-op and the function executes as usual.
Use @agentic_function(resumable=True) for synchronous orchestration whose external work is inside explicit steps:
from openprogram import agentic_function
from openprogram.agentic_programming.continuation import step
@agentic_function(resumable=True)
def report(topic: str):
research = step("research", collect_research, topic)
return step("write", write_report, research)Define collect_research and write_report as ordinary source-defined functions. Step inputs and results must be JSON-compatible. A completed step returns its saved result on continuation, without calling its action again. Repeated step names are distinguished by occurrence; keep their order and completed inputs stable. Put nested orchestration in workflow("name", function, *args, **kwargs). parallel({"branch": (function, args, kwargs)}) runs named workflows concurrently and waits for every branch before releasing ownership. Each branch has independent durable progress.
In Settings, execution.code_change_policy selects keep_original (default) or use_latest. The task's Continue control can override that policy. Python function and helper source is retained with the execution. After adopting B, a later restart with keep_original retains B, even if the installed code is now C. A candidate that fails compatibility before adopting any remaining step does not replace the retained active version. Imported module identities are checked; an unavailable or changed pinned dependency pauses recovery. With use_latest, completed results remain saved and the current function executes the remaining compatible steps. Deleted or reordered recorded steps and changed completed inputs pause the task instead of repeating an action. Changes to permissions or tool parameter contracts still require compatibility checks. If B needs a different local state shape, write that conversion as pure orchestration over saved JSON before its remaining steps.
Manual calls resume the same execution and function record. Chat calls return their eventual result to the original pending tool call. The existing two-hour automatic restart window applies to restart-owned interruptions. Explicit pauses, cancellation, unanswered approvals, and uncertain external results do not become automatic retries.
This contract restores execution at explicit steps, not arbitrary Python stack frames. Calls and external mutations outside steps, asynchronous orchestration, generators, live handles, and non-JSON results are not supported. If a process dies after an external action starts but before its result is committed, recovery requires reconciliation; it cannot assume that action failed. The function run dialog distinguishes functions that declare this contract from ordinary functions that require a new run after interruption.
Do not share mutable Python globals, closures or defaults between steps. Pass persistent state through JSON step inputs and results; process-local mutations are not a recovery protocol.
Import step dependencies at module scope. Imports inside retained helpers, dynamic import APIs and generated code are rejected before executing steps. Source-defined Python helpers are retained recursively. Opaque module and class dependencies currently support only the pinned standard library; third-party and user package objects are rejected because an initializer alone does not identify their implementation.