OpenProgram Docs

Runtime#

Source: openprogram/agentic_programming/runtime.py

LLM 运行时。封装 LLM provider,自动从 session DAG 算上下文、调用 LLM、把回复写回 DAG。


Class: Runtime#

class Runtime(call=None, model="default", max_retries=None, api_key=None, skills=None)

构造参数#

参数 类型 默认值 说明
call Callable | None None 用户自带的 LLM 函数。签名:fn(content: list[dict], model: str, response_format: dict) -> str。内部经 CallableModel 包进标准 provider 路径,DAG 记录和历史渲染与真 provider 一致。既没传 call 也没用 "provider:model_id" 形式时,需要子类化并重写 _call()
model str "default" 默认模型。两种形式:"provider:model_id"(如 "anthropic:claude-sonnet-4-6")经 openprogram.providers 解析并走 provider 层流式;其它字符串只有配合 call= 或子类才有意义。未知的 "provider:model_id"ValueError
max_retries int | None None exec() 最大尝试次数(包含首次调用,且必须 >= 1)。None = 读环境变量 OPENPROGRAM_MAX_RETRIES,没设则为 6
api_key str | None None provider 路径的 API key。None = 从凭据库解析(openprogram providers login)
skills bool | list[str] | None None system prompt 的技能发现。None / False = 关闭;True = 探测默认技能目录(用户 + 仓库);list[str] = 显式目录列表。开启后每次 exec() 的 system prompt 追加 <available_skills>

属性#

属性 类型 说明
model str 默认模型名称
max_retries int 解析后的重试预算
system str 可赋值的 system prompt,exec() 在 provider 路径上读取(@agentic_function(system=...) 装饰器会在调用期间设置它)
thinking_level str 推理力度旋钮:"off"(默认)/ "low" / "medium" / "high" / "xhigh",透传给 provider
session_id str 跨多次 exec() 稳定的 id("op-<hex>"),provider 拿它当 prompt-cache key
on_stream Callable | None 可选回调 fn(event_dict),接收流式事件(text / thinking / tool_use / tool_result)
last_usage dict | None 上一次调用的 token 用量:{input_tokens, output_tokens, total_tokens, cache_read, cache_create, ...}

方法#

exec()#

Runtime.exec(content, context=None, response_format=None, model=None,
             tools=None, toolset=None, tools_source=None, tools_allow=None,
             tools_deny=None, tool_choice="auto", parallel_tool_calls=True,
             max_iterations=20, choices=None, timeout_s=None, on_retry=None,
             web_search=False, stream_fn=None) -> Any

调用 LLM,上下文从 session DAG 自动算出。

@agentic_function 内部调用时:

  1. 从当前函数的 DAG 节点出发,render_contextexpose / render_range 算出本次要读哪些历史节点
  2. render_dag_messages 把这些节点渲染成 messages
  3. 调用 _call() 发送请求
  4. 回复写进 exec() 开始时打开的那个 llm 节点

没安装 DAG store 时(standalone 脚本、无 dispatcher):content 包成单条 user message 作单轮调用发送,不做任何记录。

一个 @agentic_function 可以多次调用 exec(),每次都是 DAG 上的一个新 llm 节点。

参数

参数 类型 默认值 说明
content list[dict] | str (必填) 内容块列表(见下方格式)。纯字符串会包成一个 text 块
context str | None None 遗留参数,已被忽略——provider 路径从 DAG 构建历史
response_format dict | JsonSchemaOutput | None None 裸 JSON Schema 或规范化 JsonSchemaOutput 包络。已验证的 provider/model 组合使用已注册的原生映射;否则 fallback="auto" 可使用已验证的隐藏 strict-tool 路径,提示词回退必须显式设置 fallback="prompt"。不支持或有损的组合直接失败。终态按原始 schema 做本地解析与校验,并返回 Python JSON 值;max_validation_retries 只能是 01。同时仍转发给 _call() 供子类使用
model str | None None 覆盖默认模型
tools list | None None 本次调用 LLM 可用的工具。每项可以是 @agentic_function{"spec":..., "execute":...} 字典、或带 .spec / .execute 的对象。设了就跑工具循环直到模型返回纯文本。默认(None)不是"无工具":调用会拿到完整的注册工具集;纯推理调用传 toolset="none",要显式空列表传 tools=[]
toolset / tools_source / tools_allow / tools_deny None 工具集预设与策略过滤:toolset 指名预设("full" 是隐式默认,"none" 表示退出),tools_source 按渠道来源过滤,tools_allow / tools_deny 是名单允许/拒绝列表
tool_choice str | dict "auto" "auto" / "required" / "none" / {"type":"function","name":"X"} 强制某工具。透传到 provider(OpenAI / Anthropic / Gemini / Bedrock 各自映射协议形态)
parallel_tool_calls bool True 允许一轮多个工具调用;False 透传到支持该开关的 provider
max_iterations int 20 工具循环轮数上限(一轮 = 一次模型调用 + 其工具执行)。生效值为 max(1, min(50, max_iterations)),50 是 agent_loop.py 的硬上限
choices dict | list | None None 设了则约束 turn 的收尾:模型跑完整 turn 后,最终回复必须从 choices 里选一个;exec 解析并返回该选择的结果。详见 next-step-decision
timeout_s float | None None 整个 exec()(含全部重试休眠)的墙钟时间预算,超时抛 LLMError(reason=TIMEOUT, retryable=False)。None = 回落到环境变量 OPENPROGRAM_EXEC_TIMEOUT_S(没设或为 0 = 不限时)
on_retry Callable | None None 每次退避休眠前调用的观测回调(每个后面还排着重试的失败尝试触发一次),入参 RetryInfo;最终失败不触发。回调内抛出的异常被吞掉
web_search bool False 本次调用启用 provider 原生的 web 搜索工具(视 provider 支持)
stream_fn None 逐调用的流函数覆盖(dispatcher 和测试用它注入假的或预构建的流);None = 真 provider

Content block 格式

{"type": "text",  "text": "Find the login button."}
{"type": "image", "path": "screenshot.png"}
{"type": "image", "data": "<base64>", "mime_type": "image/png"}
{"type": "video", "path": "clip.mp4"}
{"type": "audio", "path": "recording.wav"}

媒体块给 path(自动读取并 base64 编码,mime 类型按扩展名猜)或内联 data + mime_type 都行。text 块可带 "role": "system" 汇入 system prompt;text/image 块接受 cache_control 做 provider prompt caching。未知块类型静默跳过。

返回值

response_format=None 时返回 LLM 回复文本 str。传结构化 response_format 时返回经过本地校验的 Python JSON 值(dictlist、标量或 None)。带 choices 时返回解析后的决策结果(选中函数的返回值,或选中值本身)。

异常

  • RuntimeError — runtime 已关闭(调用过 close())
  • TypeError / NotImplementedError — 立即抛出,从不重试(编程错误:调用签名不对、没配置 provider)
  • LLMError — 重试耗尽或遇到不可重试错误时抛出,结构化字段含 reason / retryable / http_status / retry_after_s / attempts / elapsed_s / provider / model
  • StructuredOutputSchemaError — 公共 schema 或包络非法;稳定 code="invalid_schema"
  • StructuredOutputUnsupportedError — 没有已验证且无损的 provider/fallback 合同;稳定 code="unsupported"
  • StructuredOutputValidationError — JSON 非法、schema 不匹配或隐藏提交合同失败;稳定 code 为 invalid_jsonvalidation_failedmissing_submissionmixed_submission
  • StructuredOutputGenerationError — provider 拒绝或未完整生成;稳定 code 为 refusalincomplete

全部 structured-output 异常都提供稳定 .code 和有界 .issues 字段。 provider 候选正文不属于公共诊断合同。


async_exec()#

await Runtime.async_exec(content, context=None, response_format=None, model=None,
                         timeout_s=None, on_retry=None) -> Any

exec() 的异步版本,具有相同的 response_format=None 文本返回以及结构化 Python JSON 返回/异常合同。内部调用 _async_call();call= 函数可同步或异步, 默认 provider 路径使用同一 AgentSession structured lifecycle。timeout_s / on_retry 语义与 exec() 相同;重试用 asyncio.sleep 休眠,外部取消能生效。 它没有公开的工具循环参数。


_call()#

Runtime._call(content, model="default", response_format=None) -> Any

实际调用一次 LLM 的方法(不含重试——重试循环在 exec() 里包着)。默认实现在配置了 provider 模型或 call= 函数时走 provider 层(AgentSession),否则抛 NotImplementedError子类化时重写此方法。

参数

参数 类型 说明
content list[dict] 当前 turn 的内容块(历史由 provider 路径从 DAG 渲染)
model str 模型名称
response_format dict | None 输出格式约束

返回值

普通调用返回原始文本;结构化格式激活时返回已校验的 Python JSON 值。


_async_call()#

await Runtime._async_call(content, model="default", response_format=None) -> Any

_call() 的异步版本。子类化时重写此方法以支持异步 provider。


close()#

释放资源、结束会话;close() 之后 exec()RuntimeErrorRuntime 也是上下文管理器(with Runtime(...) as rt: 退出时自动关闭)。子类重写它清理 provider 专属资源。


向用户提问#

有前端会话连着时,runtime 可以在函数中途阻塞等用户输入:runtime.ask(prompt, options=..., multi=..., questions=[...], timeout=300.0, default=None)(一题或多题一屏),runtime.confirm(prompt, default=False)(是/否),runtime.form(prompt, fields)(多字段表单)。runtime.can_ask() 报告当前有没有人能回答(headless 跑时为 False)。用户拒绝抛 UserDeclined;超时给了 default 就返回它,否则抛 AskTimeout


使用方式#

方式一:传入 call 函数#

from openprogram import agentic_function
from openprogram.agentic_programming.runtime import Runtime

def my_llm(content, model="sonnet", response_format=None):
    # 把 content 转成你的 provider 格式,发请求
    texts = [b["text"] for b in content if b["type"] == "text"]
    return call_my_api("\n".join(texts), model=model)

runtime = Runtime(call=my_llm, model="sonnet")

@agentic_function
def observe(task):
    """Look at the screen."""
    return runtime.exec(content=[
        {"type": "text", "text": f"Find: {task}"},
        {"type": "image", "path": "screenshot.png"},
    ])

方式二:子类化#

class MyAnthropicRuntime(Runtime):
    def __init__(self, api_key, model="sonnet"):
        super().__init__(model=model)
        self.client = anthropic.Anthropic(api_key=api_key)

    def _call(self, content, model="sonnet", response_format=None):
        messages_content = []
        for block in content:
            if block["type"] == "text":
                messages_content.append({"type": "text", "text": block["text"]})
        response = self.client.messages.create(
            model=model, max_tokens=1024,
            messages=[{"role": "user", "content": messages_content}],
        )
        return response.content[0].text

runtime = MyAnthropicRuntime(api_key="sk-...", model="claude-sonnet-4-6")

多个 Runtime 共存#

fast = Runtime(call=gemini_call, model="gemini-2.5-flash")
strong = Runtime(call=claude_call, model="sonnet")

@agentic_function
def observe(task):
    """Quick observation with cheap model."""
    return fast.exec(content=[...])

@agentic_function
def plan(goal):
    """Complex planning with strong model."""
    return strong.exec(content=[...])

Retry 机制#

exec()async_exec() 内置自动重试,用于处理 LLM API 的临时性错误(网络超时、速率限制、服务器错误等)。

配置#

# 默认:max_retries=None → 读环境变量 OPENPROGRAM_MAX_RETRIES,没设则为 6
rt = Runtime(call=my_llm)

# 不重试(失败即抛异常)
rt = Runtime(call=my_llm, max_retries=1)

# 多次重试(适用于不稳定的 API)
rt = Runtime(call=my_llm, max_retries=5)

行为规则#

情况 处理
API 调用成功 返回结果
API 抛出瞬态异常 记录失败 attempt,按指数退避休眠(基数 1.5 秒 x 2^attempt,±25% 抖动;服务端 Retry-After 提示作为下限被遵守;基数可用 OPENPROGRAM_RETRY_BACKOFF_BASE 调),然后重试直到达到 max_retries
永久性错误(图片数据损坏、登录过期、无效 API key、或 provider 已把异常标记 retryable=False) 立即以 retryable=FalseLLMError 抛出,不重试
provider 自己的传输层重试预算已耗尽(transport_exhausted) 不再二次重试——直接抛 LLMError
TypeErrorNotImplementedError 立即抛出,不重试(通常是 provider 实现或调用方式的问题)
所有重试均失败 抛出结构化 LLMError(reason / retryable / http_status / attempts 等字段),并附上完整 attempt 报告

错误报告格式#

当所有重试耗尽时,抛出的 LLMError 包含每次尝试的错误信息,结构化字段(reason / retryable / http_status / attempts / elapsed_s 等)可直接读取:

LLMError: exec() failed after 3 attempt(s):
Attempt 1: ConnectionError: timeout
Attempt 2: RateLimitError: 429 Too Many Requests
Attempt 3: ConnectionError: timeout

重试的边界#

max_retries 只处理 API 层面的瞬态故障(网络超时、速率限制等)。如果是函数本身的逻辑或输出格式有问题,重试解决不了——直接修改函数代码,参见 skills/agentic-programming/SKILL.md

Last updated · 2026-08-13