Skip to content

Latest commit

 

History

History
106 lines (75 loc) · 10.9 KB

File metadata and controls

106 lines (75 loc) · 10.9 KB

agentic_function

Source: openprogram/agentic_programming/function.py

@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.

Usage

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(...).

Decorator parameters

Agentic-specific parameters

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)

Tool-registration parameters

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).

Runtime injection

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.

Introspection and safety

  • 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 raise CancelledError to abort it (this is how the WebUI stop button works).

Recording to the DAG

  • Entering the function: write a code node (output=None, status="running"), and store the function docstring into that node's metadata.doc, which is prepended to function_name(args) when rendering context.
  • llm() inside the function body: each call writes an llm node.
  • Exiting the function: backfill the same code node's output / 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.

Durable steps and code selection

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.