# Design philosophy

> OpenProgram is the productized implementation of the Agentic Programming paradigm.
> This document is about the paradigm itself: what problem it solves, why it inverts control, and what its core primitives are.
> The paradigm is described in the paper *LLM-as-Code: Agentic Programming for Agent Harness* ([arXiv:2606.15874](https://arxiv.org/abs/2606.15874)), accepted at the KDD 2026 AgenticSE workshop.

## The Problem

Every current LLM Agent framework hands control to the model:
- **What to do** is decided by the LLM (a planner plans first, then the agent executes)
- **When to do it** is decided by the LLM (a while loop runs until the agent says "I'm done")
- **How to do it** is decided by the LLM (tool calls, arguments, ordering)

The cost:
- **Unpredictable execution** — the same input produces a different trajectory every time
- **Context explosion** — every step crams the history back into the model
- **No output guarantees** — no one can say "this task is guaranteed to run to completion"
- **Debugging hell** — when something breaks, you can't tell whether it's a prompt problem, a tool problem, or a model hallucination

The root cause: **using a black-box probabilistic system to do work that could have been done with deterministic code in the first place**.

## The Inversion: Python Controls Flow, the LLM Reasons

Agentic Programming gives control back to the programmer:

| Dimension | Traditional Agent | Agentic Programming |
|------|-----------|---------------------|
| Flow | LLM plans | Python code |
| Decisions | LLM judges at every step | Python decides whether to call the LLM |
| State | Crammed into the context | Function variables, return values |
| Testability | Prompt regression | Unit tests |

Decompose a complex task into a function call graph. For each node on the graph, you decide:
- **Doesn't need reasoning** — use a plain Python function
- **Needs understanding / generation / judgment** — decorate it with `Agent` method, and call `llm(...)` inside the function body

The LLM becomes a tool that you call, constrain, and compose.

## The Three Primitives

The entire paradigm is just three things:

### 1. `Agent` method

A decorator. For a function it decorates, the docstring travels with the call as descriptive context, and `llm(...)` inside the body triggers one model call. The decorator supplies the ambient runtime.

```python
from openprogram import Agent
from openprogram.agentic_programming import llm

class ExampleAgent(Agent):
    method_options = {
        'summarize': {'tool': True},
    }

    def summarize(self, text: str) -> str:
        """Summarize a text in one sentence, preserving the core point."""
        return llm([{"type": "text", "text": (
            f"Summarize in one sentence, preserving the core point:\n\n{text}"
        )}])

_example_agent = ExampleAgent()
summarize = _example_agent.summarize
```

External callers can't tell the difference — `summarize(article)` looks just like any Python function.

### 2. `Runtime`

The runtime abstraction for LLM calls. It is responsible for:
- Packaging the current conversation history
- Calling the underlying provider (Anthropic / OpenAI / Claude Code / ...)
- Writing the result back into the context

User orchestration calls `llm()` for a single model request. `Runtime.exec()` remains the lower-level embedding and infrastructure API used underneath it.

### 3. `Context`

Each Agent execution owns its DAG. Chat records user messages, model requests and directly dispatched tools; ordinary helpers create no durable nodes. Calling another Agent creates an independent DAG; the parent stores the invocation result and a child_session_id reference. Permission, cancellation and file-checkpoint ownership remain with the originating execution, independent of graph ownership.

The DAG is not just a trace — it is also **where each LLM call's history comes from**: `llm()` renders its message history from the DAG through the ambient runtime. Two decorator knobs shape that flow per function:

- `expose` — what a completed call reveals to its parent (`"io"` by default: name + input + output, internals hidden).
- `render_range` — how much history the function's own `exec` pulls in. `render_range={"callers": 0}` gives an isolated scratch context that sees none of the prior conversation.

So context management stops being prompt plumbing and becomes a property you declare on the function. The same DAG doubles as your debugging view: visualization, token accounting, replaying failure paths.

## Derived Concepts

### The LLM Writes Code Too

The LLM isn't just the runtime's reasoning engine; it can also **write code** — generating, modifying, and fixing `Agent` methods that conform to the documented API. This needs no dedicated `create()` / `fix()` framework functions; the agent authors new functions with ordinary file edits. A background watcher rescans `programs/workflow/` and hot-loads new modules: their Agent method executions fire on import and self-register, so a freshly written function is callable without a restart.

Code is data, the LLM is the compiler, and functions are the product — the loop closes.

### Dual Mode

Agentic Programming is at the same time:
- **A library** — you write `Agent` methods and wire up the pipeline by hand
- **A running product** — chat in the CLI or WebUI and ask the agent to write the function for you; the generated file lands in `programs/workflow/` and hot-loads

Beginners start by asking, and what they get is a complete, readable Python file. Those who want to dig deeper can then import and hand-write. This is a tool that **can be understood incrementally**.

## Comparison with Traditional Agent Frameworks

| Scenario | LangChain / AutoGPT | Agentic Programming |
|------|---------------------|---------------------|
| "Fetch 10 pages and generate a summary for each" | The agent decides ordering and parallelism itself | Python writes `for url in urls: summarize(fetch(url))` |
| "Remember context across 3 consecutive conversations" | Stuff the conversation into a memory store and query it each time | It's just a local variable in a Python function |
| "Let the LLM decide which tool to call" | function calling + agent loop | `agent(prompt, tools=[...])` or `decision.make(prompt, options)` |
| "Retry on error" | The agent decides itself | `try / except` + code gates: an invalid pick is caught by validation and the model is asked to re-decide |

This isn't to say agent frameworks are wrong; they suit a class of tasks (fully open-ended, with fuzzy goals). But most of what you want to do can actually be done more reliably with Agentic Programming.

## OpenProgram = the Productized Paradigm

The `agentic_programming/` subpackage is the paradigm's engine code. `context/` implements the flat-DAG context model. `providers/` adapts the various LLMs. `programs/workflow/` holds the functions and applications already written under this paradigm. `webui/` lets beginners run things without writing code.

The paradigm comes first; the product exists to use it.

---

Further reading:
- [Getting Started](../../start/GETTING_STARTED.md)
- [API Reference](../../reference/api/)
- [Design Details](../../reference/design/)
