# Agent 与 Context

`agent()` 执行模型工具循环。`Agent` 保存可复用配置，并为普通子类方法建立作用域。`Context` 提供命名内容与历史选择。所有入口复用现有 Runtime 和 Session DAG。

## 直接调用与 Agent 类

普通调用不需要手动构建或绑定 Context。

```python
from openprogram import Agent, agent, agent_async

answer = agent("Summarize this text", tools=[])

class Researcher(Agent):
    instructions = "Return a short, factual answer."
    tools = []

    def prepare(self, question):
        """Remove surrounding whitespace."""
        return question.strip()

    def research(self, question):
        """Answer the prepared question."""
        return self(self.prepare(question))

researcher = Researcher(model="configured-model")
answer = researcher.research("A question")
# 异步代码：answer = await researcher.arun("A question")
# 或：answer = await agent_async("A question", tools=[])
```

`agent()` 接收字符串或内容块，可指定 `model`、`effort`、`tools`、`choices` 和 `runtime`。`choices` 让收尾回复成为一次[下一步决策](../../capabilities/agentic-programming/choosing-the-next-step/next-step-decision.zh.md)，并返回解析后的选择结果。工具执行选项见 [Runtime](runtime.zh.md)。`tools=None` 选择可用工具，`tools=[]` 禁用工具。`agent_async()` 使用同样的选项。

`Agent(model=..., instructions=..., context=..., tools=..., runtime=..., effort=..., **options)` 用实例设置覆盖类默认值，调用参数再覆盖实例设置。`Agent.from_spec(spec, **overrides)` 使用现有 AgentSpec 配置，不创建已保存 Agent 条目。

构造对象不请求模型，也不创建执行会话。调用继承当前执行；没有外层执行时创建独立执行，并在结束时释放资源。复用实例不自动续接上一次独立会话。

普通实例方法、静态方法与类方法自动建立调用作用域，包括单下划线帮助方法。双下划线特殊方法与生成器不采集。方法记录不会注册模型工具。在对应 `method_options` 设置 `"tool": True` 可显式登记工具。 `method_options = {"method_name": {"expose": "io", "render_range": {...}}}` 无需装饰器即可设置方法元数据。

## Context 内容与可见性

框架自动派生并绑定 Context。显式配置是可选的：

```python
from openprogram import Agent, Context

context = Context(
    {"notes": "Use the project terminology."},
    providers={"current_topic": lambda current: "Agent context"},
)
researcher = Agent(context=context, tools=[])
```

`Context(blocks=None, *, parent=None, providers=None, history_filter=..., call_id=..., store=None, head_id=None)` 是命名内容的可变映射。`derive()` 创建具有独立内容的子上下文。`Context.current()` 返回当前任务绑定，无绑定时返回 `None`。`merge(other)` 覆盖命名内容和显式选择规则，不替换作用域调用身份。宿主集成可使用 `bind()` 显式绑定。

提供函数是接收当前 Context 的同步 Python callable，在每次模型请求时运行。提供函数失败会终止该请求。框架不执行表达式字符串。

`history_filter` 默认为 `"dag"`。`"current_call"` 从已可见的 DAG 节点中选择当前作用域与其后代调用，`False` 禁用历史。callable 接收 `(node, context)`，在已有 DAG 可见性检查之后过滤节点。这些设置不能扩大工具权限。

Context 使用同一模型：父继承、命名内容、请求时求值和历史可见性。Agent 实例内容是该模型的实例绑定。模型交互仍归执行会话保存。内容与系统指令、工具权限分别处理。

## 显式会话生命周期

多次独立调用需要续接同一会话时，使用 `Context(store=writer, head_id=None)`。writer 是现有 `SessionNodeWriter`：

```python
from openprogram import Agent, Context
from openprogram.store import SessionStore, SessionNodeWriter

store = SessionStore(root_path="/var/lib/myapp/sessions")
store.create_session("research", agent_id="main")
writer = SessionNodeWriter(store, "research")
researcher = Agent(context=Context(store=writer), tools=[])
try:
    first = researcher("List the project requirements.")
    second = researcher("Review the requirements from the previous call.")
finally:
    store.close()
```

`head_id` 选择已有分支末端作为初始前序节点。`None` 续接当前会话末端。调用者拥有并关闭显式 store，执行作用域不会关闭它。

该显式 Context 会话提供 NOOA 式重复实例调用需要的事件生命周期，复用已有 Session DAG。Agent 仍保存配置，默认调用保持独立。

## 受管 Program 源码

受管加载器采集显式授权包目录内的源码函数，包括已选择的第一方源码、已安装及目录登记的 Program 包、所有者登记的外部 harness、已发布 Program 与保留源码快照。采集包含子模块与嵌套源码定义。任意宿主文件、无关依赖、lambda、动态生成代码与不可用源码不在该范围内。

包通过 `PROGRAM_ENTRIES` 列出公开入口。普通入口可用显式 `__agent_options__` 映射提供已有入口元数据。被采集的帮助函数不会自动成为工具。加载器保留方法配置，并排除生成器采集。

内置 text workflow 使用普通 `TextAgent` 方法。`summarize_text` 调用 `agent(..., tools=[])`。模块导出保留公开名称与表单元数据。

类与方法接口参考 [NVIDIA-labs OO Agents](https://github.com/NVIDIA-NeMo/labs-OO-Agents)。OpenProgram 不声称 NOOA API 兼容。

## 方法配置

普通 Agent 方法产生 code 节点，模型请求产生 llm 节点。Agent 承担方法元数据、执行控制与显式登记。Context 动态构建每次请求。

Agent 定义方法执行、配置与登记，Context 动态构建每次请求。

### 用法

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

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

    def f(self, x: str) -> str:
        """One-line summary of what f does."""
        return llm([{"type": "text", "text": f"...{x}..."}])

_example_agent = ExampleAgent()
f = _example_agent.f
```

每个普通方法自动建立调用作用域。通过类的 `method_options` 映射配置指定方法。

### 方法配置

### 执行与 Context 设置

| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `expose` | `str` | 普通方法 `"full"`；登记工具 `"io"` | **朝外**:别人渲染 DAG 时能看到我的什么。`"io"` = 对外只露函数名和返回值,内部(LLM 交换、子调用)隐藏;`"llm"` = 反过来,只露内部 LLM 交换,藏函数自己的名字/返回值和嵌套的 code 子调用;`"full"` = 全可见(docstring + 参数 + 输出 + LLM 回复 + 内部);`"hidden"` = 根本不写 DAG 节点。其余值在装饰时抛 `ValueError` |
| `render_range` | `dict` | `None` | **朝内**:本函数内部 `llm()` 拼 prompt 时,从 DAG 读多少历史节点。形状 `{"callers": N, "subcalls": M}`,两个数字都是 **节点计数(按 `seq` 切片)**:<br>• `callers` — 本函数 frame **启动前**的节点,取最近 N 个(`None` 默认 = 不限,`0` = 全墙)<br>• `subcalls` — 本函数 frame **启动后**已写入的节点,取最近 N 个(`-1` 默认 = 不限,frame 自然看见自己的进度;`N>=0` = 只想截 prompt 时显式设;`0` = 完全墙掉 in-frame)<br>`{"callers":0,"subcalls":0}` = 跟外界和自己 frame 全断绝 |
| `input` | `dict` | `None` | 每个参数的 UI 元数据,WebUI 据此渲染输入表单。每个参数支持的字段:`description`(参数名旁的标签)、`placeholder`(示例文字)、`multiline`(`True` = textarea)、`options`(允许值列表,渲染为下拉框并写进 JSON-schema `enum`)、`hidden`(`True` = 从表单和 LLM 工具 schema 里排除) |
| `system` | `str` | `None` | 本函数 LLM 调用的 system prompt(调用期间盖到注入的 runtime 上,调用后恢复) |

### 工具注册参数

设置 `tool=True` 的 Agent 方法注册进共享注册表(`openprogram.programs`),成为 LLM 可调用的工具,与 `@function` 装饰的工具并列。以下参数控制这次注册,名字和语义与 `@function` 一致:

| 参数 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `tool` / `as_tool` | `bool` | `False` | 注册为 LLM 可调用工具。`False` = 只能 Python 直接调用 |
| `name` | `str` | `None` | 工具名覆盖。默认取函数的 `__name__` |
| `description` | `str` | `None` | 工具描述覆盖。默认取函数 docstring |
| `parameters` | `dict` | `None` | JSON-schema 参数覆盖。默认由签名类型注解加 `input` 元数据自动生成(runtime 注入参数和 `hidden` 参数被排除) |
| `label` | `str` | `None` | 工具 UI 里显示的可读标签 |
| `toolset` | `tuple` | `()` | 本工具所属的工具集名(供 `exec(toolset=...)` 预设使用) |
| `unsafe_in` | `tuple` | `()` | 在哪些渠道来源下视为不安全并被过滤 |
| `check_fn` | `Callable` | `None` | 逐次调用的门禁:分发前调用,返回假值则拦下 |
| `requires_env` | `tuple` | `()` | 必须设置的环境变量名,缺了就不提供该工具 |
| `can_use` | `Callable` | `None` | 工具解析时求值的动态可用性谓词 |
| `max_result_chars` | `int` | `None` | 回喂给模型的工具结果截断上限。`None` = 注册表默认 `DEFAULT_MAX_RESULT_CHARS`(30,000 字符) |
| `persist_full` | `bool` | `False` | 把未截断的完整结果落盘,供 agent 回读 |
| `head_ratio` | `float` | `None` | 截断时头部保留的比例,其余留尾部。`None` = 注册表默认 `DEFAULT_HEAD_RATIO`(0.7) |
| `requires_approval` | — | `None` | 转发给工具注册表的审批要求(与 `@function` 同形) |
| `cache` | `bool` | `False` | 工具分发调用按 `(name, args)` 记忆化结果 |
| `cache_ttl` | `float` | `300.0` | `cache=True` 时的缓存寿命(秒) |
| `timeout` | `float` | `None` | 工具分发调用的硬性墙钟超时(秒),到点模型收到错误结果 |
| `available_if` | `Callable` | `None` | 工具登记门禁：返回假值或抛异常时不登记工具，普通方法作用域仍有效 |
| `defer` | `bool` | `False` | 注册为延迟工具(schema 按需加载,不随每次调用发送) |
| `register_globally` | `bool` | `True` | `False` = 构建工具但不进全局注册表 |

函数名、参数名 / 类型 / 默认值、一句话摘要都从函数签名和 docstring 自动读取,不在 method_options 中重复。

### 选择 Runtime

方法在它所属 Agent 配置的 Runtime 上运行;Agent 没有配置时用调用方的:`ExampleAgent(runtime=rt).f("x")` 让 `f` 及其中每次模型请求都在 `rt` 上运行,它调用的 Agent 方法也继承 `rt`。Runtime 在程序入口(CLI、脚本、测试)配置,不通过方法参数往下传。一个工作流要用两个模型时——比如作者和评审——给第二个角色一个自带 Runtime 的 Agent 实例。

### Runtime 注入

旧函数仍可能声明名为 `runtime`、`exec_runtime` 或 `review_runtime` 的参数,它们会自动注入:调用方没传(或传 `None`)时,先从当前调用链取 runtime;作为入口调用时经 `create_runtime()`(自动检测)新建,函数返回时再关闭。一个函数可以声明多个 runtime 参数,全部填同一个 runtime。这些参数不会出现在 LLM 工具 schema 和 WebUI 表单里。新写的方法不声明这些参数。

### 自省与安全

- `fn.spec` — 自动生成的 JSON-schema 工具 spec(`{"name", "description", "parameters"}`);`fn.execute(**kwargs)` 用 LLM 提供的 kwargs 调用 wrapper。
- 自递归兜底:函数自我重入超过 5 层抛 `RecursionError`(注入的情境 prompt 也会引导模型不要自调用)。
- 预调用钩子(`add_pre_invocation_hook` / `remove_pre_invocation_hook`)在每次调用开头运行,可以抛 `CancelledError` 中止本次调用(WebUI 的停止按钮就是这么实现的)。

### 记录到 DAG

- **进入函数**:写一个 `code` 节点(`output=None`, `status="running"`),函数 docstring 一并存进该节点的 `metadata.doc`,渲染上下文时拼在 `函数名(参数)` 前面。
- **函数体内 `llm()`**:每次调用写一个 `llm` 节点。
- **退出函数**:回填同一个 `code` 节点的 `output` / `status`。

`expose="hidden"` 时不写任何节点。standalone 运行(没安装 DAG store)时记录全部 no-op,函数照常执行。

### 可恢复步骤与代码版本

为同步编排函数声明 `method_options = {"report": {"resumable": True}}`，将外部操作放入 `step("稳定名称", 操作函数, *args, **kwargs)`。步骤输入和结果必须可保存为 JSON；继续执行时，已完成步骤直接返回保存的结果，不再调用操作函数。重复名称按出现次数区分，已执行步骤的顺序和输入必须保持兼容。

嵌套编排使用 `workflow("名称", 函数, *args, **kwargs)`。并行编排使用 `parallel({"分支名": (函数, args, kwargs)})`；各分支独立保存进度，所有分支停止后才释放任务所有权。

设置项 `execution.code_change_policy` 默认是 `keep_original`，继续使用保存的原函数和 Python 辅助函数源码；`use_latest` 保留已完成步骤的结果，并用当前代码执行后续兼容步骤。任务的“继续”操作可以覆盖此设置。A 已采用 B 后，再更新到 C 时选择保留原代码，会继续 B；尚未通过已有进度兼容性检查的候选版本不会取代已采用版本。新代码需要转换局部状态时，应在剩余步骤之前通过纯编排代码转换已保存的 JSON。已记录步骤被删除、重排、输入不兼容或固定依赖不可用时，任务保持暂停。权限和工具参数约定仍然需要通过兼容性检查。

手动调用恢复原执行记录；聊天调用将最终结果交给原来的待完成工具调用。重启导致的中断沿用两小时自动继续期限。主动暂停、取消、未回答的授权请求以及结果不确定的外部操作不会自动重试。

此功能在显式步骤处恢复，不保存任意 Python 调用栈。步骤外的外部调用和状态修改、异步编排、生成器、活动句柄及非 JSON 结果不支持此恢复约定。外部操作已经开始、但结果尚未保存时进程退出，必须先核对操作结果。函数运行界面显示是否声明可恢复步骤，普通函数中断后需要重新运行。

步骤之间不得依赖可变的 Python 全局变量、闭包或默认参数。需要持续保存的状态应通过步骤的 JSON 输入和结果传递，进程内变量修改不能作为恢复依据。

步骤依赖需要在模块级导入；辅助函数内部导入、动态导入 API 和动态生成代码会在执行步骤前被拒绝。Python 辅助函数源码递归保存。模块和类对象目前仅支持固定的标准库身份，第三方及用户包对象会被拒绝，因为只保存包初始化文件无法确定完整实现。

## 会话轮次与恢复

`Agent.run_turn(request, *, context=None, on_event=None, cancel_event=None, execution_context=None)` 返回 `TurnResult`。从 `openprogram` 导入 `TurnRequest` 和 `TurnResult`。新轮次执行前应用实例配置；`Context.for_session(store, session_id, head_id=..., blocks=..., providers=...)` 绑定持久化存储和分支。请求与 Context 的会话不一致时拒绝执行。`Context.session_id` 返回所选图的标识。

`arun_turn(request, **options)` 提供协作式取消的异步执行。`resume_turn(continuation, **options)` 与 `aresume_turn(...)` 复用断点请求，不重复执行准入。事件回调在执行线程接收现有运行时事件字典。持久化暂停和执行中追加输入沿用已有执行控制及安全点协议。

`ChatAgent` 继承这套契约，是实际聊天使用的类型。子类重写轮次入口时，不会将其自动包装为普通函数调用。旧 `openprogram.agent.dispatcher` 导入与共享 `openprogram.agent.turn_runtime` 指向同一模块，不再维护另一份执行循环。
