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
availableregistry 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 |
None → render_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.
Recommended style (required for new code)#
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:
- Add a shared helper
_parse_docstring_args(fn) -> dict[name, description]inagentic_programming/function.py _build_agentic_tool_speccalls this helper as a fallback (currently the spec does not read docstringArgs:at all)render_optionscalls the same helper for the same fallback- Existing
@agentic_functionfunctions 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:orReturns:sections in the docstring (unless you specifically want them for debugging / reading) - Every LLM-visible parameter has a
descriptionininput= - Enum parameters use
optionsininput=(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_functiondecorator implementationopenprogram/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 guidedocs/reference/design/function/calling-unification.md— function/tool calling frameworkdocs/capabilities/agentic-programming/choosing-the-next-step/tool-calling.md— per-turn native tool-use loop mechanics