# Function Metadata Specification

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 catalog 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={...})`
- Catalog 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, catalog menu, meta/create) 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 code generated by meta/create 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)` (catalog menu) | function name, when-to-pick description, parameter name / type / description / enum, whether each parameter is system-filled |
| `parse_args(reply, options, runtime, ...)` (catalog 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. |
| Context tree rendering | `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` |
| Context-tree expose mode | `@agentic_function(expose="io"\|"llm"\|"full"\|"hidden")` — controls what **callers** see of this function in their DAG render | `fn.expose` |
| Context-tree 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/design/function/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` → `compute_reads` 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 | none | `runtime.exec` gets NO tools unless `tools=` / `toolset=` is passed — tools are opt-in. One caveat: a nested `exec` inside a tool body inherits the outer tools 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, catalog 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 docstring as descriptive context (rendered in the tree), 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 catalog 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:
  ```python
  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:

```python
@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):

```python
@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)

```python
@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/catalog 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 context tree | yes | no |

Use plain callables for: simple decision branches with few parameters, no WebUI surface, and no need for rich catalog 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 catalog 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/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/agentic-programming/writing-functions/agentic-function.md` — decorator usage guide
- `docs/design/function/function-calling-unification.md` — function/tool calling framework
- `docs/agentic-programming/choosing-the-next-step/tool-calling.md` — per-turn native tool-use loop mechanics
