OpenProgram Docs

Function metadata#

This document defines what metadata a function in this framework must carry — whether or not it calls an LLM — where that metadata lives, and which components consume it.

Scope: every function decorated with @agentic_function, plus any plain Python callable passed into render_options or any other component of the decision-menu protocol. (runtime.exec(tools=[...]) does not accept plain callables — entries must be an @agentic_function, a {"spec", "execute"} dict, or an object with .spec/.execute; anything else raises TypeError.)

1. Why this spec exists#

Historically the same piece of information lived in multiple places:

  • Parameter descriptions could be written in the docstring Args: section or in @agentic_function(input={...})
  • Decision-menu callers hand-wrote a verbose available registry dict at the call site, repeating information already present on the function
  • Different consumers (tool_use spec, WebUI, decision menu, meta tooling) followed slightly different read-order conventions

The result: authors of new functions didn't know where to put descriptions, the framework read metadata from inconsistent sources, and generated code didn't always match what other consumers expected.

This spec defines a single source-of-truth so every consumer reads metadata by the same rules.

2. Who consumes function metadata#

Consumer Required fields
runtime.exec(tools=[fn]) (provider-native tool_use) function name, overall description, parameter JSON schema (type / required / enum / description)
render_options(options) (decision menu) function name, when-to-pick description, parameter name / type / description / enum, whether each parameter is system-filled
parse_args(reply, options, runtime, ...) (decision extract + dispatch + retry) parameter names + types + enum + hidden flag, runtime-style auto-inject allowlist
WebUI parameter form description / placeholder / multiline / options / hidden
runtime.exec rendered context docstring — carried as metadata.doc and prefixed into the rendered context text. The default system prompt comes from runtime.system (plus the skills block), not from the docstring.
Session-DAG rendering (render_context) expose mode + render_range
auto-trace / persistence function identity, argument values, return value, timing

3. Metadata fields and their canonical locations#

Every piece of information has exactly one source-of-truth. This table is the spec:

Field Where it lives How to read it
Function name def name(...) fn.__name__
Parameter names signature inspect.signature(fn).parameters
Parameter types annotation param.annotation
Parameter defaults annotation default param.default
One-line summary (what / when-to-pick) first paragraph of docstring (up to first blank line) inspect.getdoc(fn), first paragraph
Per-call LLM instructions (prompt + data for one specific runtime.exec) the content=[...] list of that exec call passed directly at call time
Per-parameter description @agentic_function(input={"x": {"description": ...}}) fn.input_meta["x"]["description"]
Per-parameter enum @agentic_function(input={"x": {"options": [...]}}) fn.input_meta["x"]["options"]
Whether the parameter is LLM-visible @agentic_function(input={"x": {"hidden": True}}) fn.input_meta["x"]["hidden"]
WebUI placeholder @agentic_function(input={"x": {"placeholder": "..."}}) fn.input_meta["x"]["placeholder"]
WebUI multiline input @agentic_function(input={"x": {"multiline": True}}) fn.input_meta["x"]["multiline"]
Dynamic option source @agentic_function(input={"x": {"options_from": "functions"}}) fn.input_meta["x"]["options_from"]
Working-directory picker mode @agentic_function(workdir_mode="optional"|"hidden"|"required") — validated by the decorator and stored on the instance fn.workdir_mode. Its consumer is the WebUI, which does not introspect the object — it AST-parses the source text (openprogram/webui/_functions.py:_extract_workdir_mode), so the value must be written as a literal in the decorator call
Framework-auto-injected parameters two constants in two files, both {"runtime", "exec_runtime", "review_runtime"}: _RUNTIME_PARAMS in agentic_programming/function.py (injection + tool-spec filtering) and _AUTO_PARAMS in agentic_programming/decision.py (menu hiding + dispatch) module level
Override of runtime.exec system prompt @agentic_function(system="...") fn.system
DAG expose mode @agentic_function(expose="io"|"llm"|"full"|"hidden") — controls what callers see of this function in their DAG render fn.expose
DAG render range @agentic_function(render_range={"callers": N, "subcalls": M}) — controls how much DAG history this function's own runtime.exec reads. Both are node-count slices on seq: callers = most-recent N nodes written before this function's frame started (default None = uncapped, 0 = wall off all prior context); subcalls = most-recent N nodes written since this function's frame started (default -1 = uncapped — the frame sees its own progress; child internals are hidden by their expose setting, not by subcalls counting; set N>=0 only to actively cap prompt size in a loop). fn.render_range
Skill trigger words / agent discovery sibling SKILL.md frontmatter loaded separately by the skill loader

Core principle: anything expressible in the signature / annotation is not repeated in the decorator; anything expressible in input= is not repeated in the docstring.

The decorator also accepts the shared tool-registration / gating kwargs (name, description, toolset, unsafe_in, requires_approval, available_if, defer, ...) documented in docs/reference/design/function/calling-unification.md; cache / cache_ttl (memoize results on name + args) and timeout (hard-kill the body after N seconds, returning an error result) behave as in @function.

Effective defaults for a bare @agentic_function#

Aspect Default Resulting behavior
expose "io" Callers see my name + input + output. My internal runtime.exec (llm Calls) are hidden from them.
render_range Nonerender_context falls through to callers=None, subcalls=-1 Pre-frame history is uncapped. In-frame nodes (the frame's own progress: earlier runtime.exec results, returned sub-function io) are also uncapped. Child @agentic_function internals stay hidden because the child carries expose="io", not because subcalls trims them.
top-level chat turn frame_entry_seq=-1, no pre-frame Same code path as any other frame — all nodes are in-frame and visible. No special-casing.
tools full toolset A bare runtime.exec with neither tools= nor toolset= resolves the full registry toolset by default — tools are ON. Pass tools=[...] for an explicit menu; pass toolset="none" or tools=[] for a tool-free reasoning call. A nested exec inside a tool body inherits the outer tools= list via the _current_tools contextvar.
system None Use the runtime's existing system prompt as-is.

Implication: a frame naturally accumulates its own work. Set subcalls=N explicitly only to bound prompt size in a long loop. Set subcalls=0 explicitly to fully wall off in-frame nodes (rare).

4. The role of the docstring vs content#

These two channels can carry overlapping information, but they have different responsibilities — neither replaces the other:

Channel Scope What goes here
docstring Whole-function level. Describes what the function as a whole does (it may run preprocessing, several LLM calls, and postprocessing). Read by humans, decision menus, tool_use specs, and meta tooling. One-line summary required. May also describe in detail what each LLM call does, expected output, edge cases — as much detail as you want, for readers and for context.
runtime.exec(content=[...]) One specific LLM call inside the function. Each exec is its own "ask"; the function may make several with different prompts. The actual prompt + data for this LLM call: what task, what output format, what constraints, plus the data to operate on. This must be written even if the docstring already describes it.

A function may have a rich docstring explaining "this function classifies sentiment by asking the LLM and normalizing the reply", but the body still needs an explicit runtime.exec(content=[...]) containing the actual instruction + data going to the LLM. Documentation in the docstring does not propagate into the LLM call — the framework sends the docstring as descriptive context (carried on the DAG node as metadata.doc and rendered into the inner call's situation prompt), not as authoritative instruction. Provider behavior varies; codex CLI in particular ignores docstring as instruction. Always put the per-call prompt in content.

Docstring writing rules:

  • One-line summary (the first paragraph) is required — that's what decision menus and tool_use specs read.
  • The body may be as detailed as is useful for code readers — including describing what each LLM call does.
  • Do not write filler ("You are a helpful assistant", "Complete the task").
  • A detailed docstring does NOT eliminate the need for explicit content=[...] text on each exec call.

content writing rules:

  • Each item is a dict like {"type": "text", "text": ...} or {"type": "image", "path": ...}.
  • Embed both the instruction and the data for this LLM call. Example:
    runtime.exec(content=[{"type": "text", "text": (
        f"Classify the sentiment of the following text. Reply with exactly one "
        f"word: positive, negative, or neutral.\n\nText:\n{text}"
    )}])
    
  • Define the exact output format inline; don't rely on the docstring or external context to convey it.

5. input= vs. docstring Args: section#

Historically the same parameter description could appear both in the docstring Args: section and in @agentic_function(input={...}). This spec makes input= the source-of-truth.

Minimal example:

@agentic_function(input={
    "text":  {"description": "Text to polish."},
    "style": {"description": "Output style.", "options": ["academic", "casual"]},
})
def polish(text: str, style: str, runtime: Runtime) -> str:
    """Polish a text in the given style."""
    ...

Fuller example exercising more metadata features (placeholder, multiline, hidden, mixed types):

@agentic_function(input={
    "essay": {
        "description": "Essay to review.",
        "placeholder": "Paste the essay text here...",
        "multiline": True,
    },
    "rubric_id": {
        "description": "Which rubric to apply.",
        "options": ["ielts_writing", "toefl_writing", "gre_argument"],
    },
    "max_score": {
        "description": "Upper bound for the numeric score.",
    },
    "show_rubric_internals": {
        "description": "Include rubric breakdown in the output.",
    },
    "session_id": {
        # System-supplied; LLM does not see this.
        "hidden": True,
    },
})
def review_essay(
    essay: str,
    rubric_id: str,
    max_score: int,
    show_rubric_internals: bool,
    session_id: str,           # filled by Python via context, not LLM
    runtime: Runtime,          # auto-injected
) -> dict:
    """Score an essay against a named rubric and return a structured report."""
    ...

The docstring keeps only the one-line summary — no Args: section and no Returns: section. If the meaning of return-value fields matters to downstream LLM-driven calls, encode it with a structured return type (e.g. TypedDict or a dataclass) so consumers can introspect it; do not duplicate the schema in a docstring paragraph.

Legacy style (still supported)#

@agentic_function
def polish(text: str, style: str, runtime: Runtime) -> str:
    """Polish a text in the given style.

    Args:
        text: Text to polish.
        style: Output style.
        runtime: LLM runtime.

    Returns:
        Polished text.
    """
    ...

Legacy functions still run, but the Args: section is dead text — no docstring-Args: parser exists anywhere in the framework (see §10). _build_agentic_tool_spec falls back as follows:

fn.input_meta[name]["description"]
    ↓ not found
fn.input_meta[name]["placeholder"]  (rendered as "e.g. {placeholder}")
    ↓ not found
no description; only parameter name + type

render_options reads only description and options from input_meta; parameters without them get name + type only.

6. Plain Python callables (undecorated)#

@agentic_function is not required for the decision-menu path: plain callables can be passed to render_options (and as decision.make / choices= options), but they carry less metadata. They can not be passed to runtime.exec(tools=[...])_adapt_tools raises TypeError for anything that is not an @agentic_function, a {"spec", "execute"} dict, or an object with .spec/.execute.

Field Decorated Undecorated
Parameter description from input= empty (no docstring fallback exists)
Parameter enum input={"x": {"options": [...]}} none
Hidden flag input={"x": {"hidden": True}} only _AUTO_PARAMS names (runtime, etc.) are auto-hidden
Recorded in session DAG yes no

Use plain callables for: simple decision branches with few parameters, no WebUI surface, and no need for rich menu hints. Upgrade to @agentic_function as soon as you need enums, hidden parameters, or detailed descriptions.

7. Auto-injected parameters#

The following parameter names are reserved by convention: if a function's signature includes any of them, the framework auto-injects them at call time. The LLM does not see them and does not need to fill them.

Parameter name Meaning
runtime The current Runtime instance
exec_runtime The runtime used for execution (multi-runtime setups)
review_runtime The runtime used for review (multi-runtime setups)

These names live in two constants in two files: _RUNTIME_PARAMS in agentic_programming/function.py (runtime injection + filtering from tool specs) and _AUTO_PARAMS in agentic_programming/decision.py (hiding from decision menus + dispatch). To add a new auto-injected name, edit both. Do not mark them per-callsite via input={"x": {"hidden": True}}.

8. WebUI rendering behavior#

The WebUI does not introspect live Python objects: it AST/regex-parses the source files (openprogram/webui/_functions.py). Consequently input= must be written as a literal dict in the decorator call to show up in the form — metadata built dynamically (variables, helper calls) is invisible to the WebUI.

The WebUI form renders each parameter by the following rules (implemented in web/components/chat/composer/modes/fn-form/fn-form.tsx and fn-form-fields.tsx). When authoring @agentic_function(input={...}), use this table to predict what kind of input control your function will produce:

Parameter trait WebUI control
bool type Yes / No toggle buttons (not a checkbox)
str type, multiline not set Defaults to a textarea (multiline=True is implied)
str type, multiline: False Single-line <input>
Non-str non-bool, multiline not set Single-line <input>
options: ["a", "b", ...] Clickable chips plus a free-form text input (user can pick a preset OR type a custom value)
options_from: "functions" <select> dropdown populated from currently-registered non-builtin / non-meta functions
hidden: True Omitted from the form entirely
Has a Python default and no explicit placeholder the raw default value becomes the placeholder ghost text (no "default: " prefix); pressing Tab in the empty field promotes it into the actual value; defaults of None or starting with _ are suppressed

The working-directory picker is rendered separately above the parameter rows, governed by the top-level workdir_mode:

workdir_mode value Form behavior
"optional" (default) Picker shown; may be left empty
"hidden" Picker not shown (function doesn't depend on filesystem location)
"required" Picker shown and required; the form blocks submission without a value

Parameter description renders as a small label next to the parameter name; the type annotation is shown on the same row, and non-required parameters get an "optional" marker (there is no "required" marker).

9. Out of scope (deferred for future expansion)#

The following fields were discussed and are intentionally not introduced now, because no consumer exists yet:

Field Intended use Why deferred
effects=["fs", "net", "state"] Mark function side effects for permission gating / dangerous-op blocking A gating surface already exists on the decorator (requires_approval, check_fn, unsafe_in, available_if, defer); a declarative effects= field on top of it remains future work
permissions=[...] Required permission scope before calling Same as above
idempotent=True Whether the function can be safely retried No auto-retry component exists
latency_hint="long" Scheduler hint No scheduler exists
cost_hint=... Quota management Same as above

Revisit this section once a real upstream consumer appears.

10. Migration path (informational)#

The recommended order for migrating existing code to this spec; not strictly required:

  1. Add a shared helper _parse_docstring_args(fn) -> dict[name, description] in agentic_programming/function.py
  2. _build_agentic_tool_spec calls this helper as a fallback (currently the spec does not read docstring Args: at all)
  3. render_options calls the same helper for the same fallback
  4. Existing @agentic_function functions do not need immediate rewriting; convert opportunistically when you touch them

11. Style checklist#

When writing a new @agentic_function:

  • First paragraph of the docstring is a one-line summary (state what the function does / when to pick it, directly)
  • No Args: or Returns: sections in the docstring (unless you specifically want them for debugging / reading)
  • Every LLM-visible parameter has a description in input=
  • Enum parameters use options in input= (not buried in the description text)
  • System-filled parameters (DB session, current user, etc.) are marked hidden: True
  • Framework auto-injected parameters (runtime, etc.) need no annotation; the framework detects them
  • The function name is clear (fn.__name__ is what the LLM sees as the action name)
  • No role-play, no empty directives, no metaphors in the docstring

12. References#

  • openprogram/agentic_programming/function.py@agentic_function decorator implementation
  • openprogram/agentic_programming/decision.py — options-menu rendering, reply parsing, and the next-step decision primitive (decision.make, render_options, parse_args, DecisionError)
  • docs/capabilities/agentic-programming/writing-functions/agentic-function.md — decorator usage guide
  • docs/reference/design/function/calling-unification.md — function/tool calling framework
  • docs/capabilities/agentic-programming/choosing-the-next-step/tool-calling.md — per-turn native tool-use loop mechanics
Last updated · 2026-08-13