> ## Documentation Index
> Fetch the complete documentation index at: https://starforge.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Agent 环境

> 编写一个让 agent 练习的任务集：manifest、任务，以及四种协议。

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf env push ./my-env --name calculator-tasks --version 1.0.0
sf env ls
sf env show alice/calculator-tasks@1.0.0
```

一个环境是**一份 taskset 加一份 manifest**，像数据集一样被拥有和版本化，
引用形式为 `<owner>/<name>@<version>`。它是 agent 用来练习的东西。
平台负责存储它、决定谁能引用它、把它服务出去——但从不编写在它里面运行的东西。

## 目录结构

```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
my-env/
├── manifest.json     # 这个环境声明了什么
└── tasks.jsonl       # 一行一个任务
```

目录下的所有东西都会被上传。版本不可变。

## tasks.jsonl

每行一个 JSON 对象：

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{"prompt": "What is 17% of 340?", "reference": "57.8", "split": "train"}
{"prompt": "Convert 45 degrees Celsius to Fahrenheit.", "reference": "113", "split": "eval"}
```

<ParamField path="split" type="train | eval" default="train">
  这一行属于 taskset 的哪一半，逐行声明。

  作业能看到哪一半是平台按操作类型决定的，所以训练作业拿不到留出任务，
  评测也要不到训练任务。评测去要一个环境从未声明过的 split 会被拒绝，
  而不是悄悄给它训练任务——那正是这个字段要防的那一种失败，
  也是唯一一种「看起来像好成绩」的失败。
</ParamField>

<Warning>
  taskset 从不外发。没有任何路由返回它的副本，也没有导出。
  一家企业认为什么算难，往往比它的训练数据更能说明问题，
  所以作业只能**引用**一个环境，任务内容只在获批的运行时里才出现。
</Warning>

## manifest.json

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "schema": "forge/environment/v1",
  "protocol": "openenv",
  "verifier": { "kind": "rubric", "ref": "alice/arithmetic-correctness" },
  "tools": [
    {
      "name": "calculator",
      "endpoint": "/tools/calculator",
      "schema": {
        "type": "object",
        "properties": { "expression": { "type": "string" } },
        "required": ["expression"]
      }
    }
  ],
  "sandbox": { "required": false },
  "description": "Arithmetic word problems with a calculator tool."
}
```

<ParamField path="schema" type="string" required>
  永远是 `forge/environment/v1`。
</ParamField>

<ParamField path="protocol" type="nemo-gym | openenv | openenv-remote | openenv-image" required>
  trainer 怎么够到这个环境。见下文。
</ParamField>

<ParamField path="verifier" type="object" required>
  `{"kind": "rubric|plugin|endpoint", "ref": "..."}`——判定任务是否完成的东西。
  见 [verifier](/zh-Hans/extend/verifiers)。
</ParamField>

<ParamField path="tools" type="array">
  agent 可以调用什么：名字、端点，以及参数的 JSON Schema。
</ParamField>

<ParamField path="harness" type="object">
  `{"plugin": "<owner>/<name>", "version": "...", "entrypoint": "module:ClassName", "digest": "..."}`。
  `nemo-gym` 必填。代码属于一个 `kind: environment` 插件；
  平台最多写一个两行的 shim 去 import 它。
</ParamField>

<ParamField path="sandbox.required" type="bool" default="false">
  harness 会运行模型生成的代码时为 true。需要沙箱的作业跑在没有配置沙箱提供方的部署上会被**拒绝**——
  它不会退化成「在训练容器里跑」。
</ParamField>

<ParamField path="domain" type="string" default="other">
  NeMo Gym 的指标分组类别。`nemo-gym` 下必填且校验，其他协议下无意义。
  取值：`math`、`coding`、`agent`、`knowledge`、`instruction_following`、`long_context`、
  `safety`、`games`、`translation`、`e2e`、`rlhf`、`other`。
</ParamField>

<ParamField path="image" type="string">
  `openenv` 环境的 OCI 镜像。和其他所有固定镜像一样按运行时产物解析，绝不临时拉取。
</ParamField>

<ParamField path="endpoint" type="url">
  `openenv-remote` 必填，其他协议下禁止。服务已经跑在哪里。
</ParamField>

## 四种协议

其中三种是作业真的能走的路径，区别在于**谁来跑这个 server**。

<Tabs>
  <Tab title="openenv —— 平台来服务">
    回合级。平台用自己持有的 taskset 启动环境 server，trainer 通过 openenv-core 的客户端
    自己驱动 episode：`reset`、`step`、`state`。

    TRL、SkyRL、Unsloth 和 Axolotl 都走这条。不需要 harness——循环本来就归 trainer 管。
  </Tab>

  <Tab title="nemo-gym —— 框架来跑">
    轨迹级（`/seed_session` + `/verify`）。NeMo Gym 会从一个配置入口启动它自己的 server，
    所以平台没有东西可服务。平台改为**物化**这个环境：
    把获授权的那个版本写进作业里，写成 NeMo Gym 期望看到的那些文件。

    轨迹循环总得有人跑，而平台不写它，所以 `harness.plugin` 必填——
    还要 `harness.entrypoint`，形如 `module:ClassName`，点名 Gym 要 import 的
    `SimpleResourcesServer` 子类。
  </Tab>

  <Tab title="openenv-remote —— 别人在跑">
    一个已经跑在别处的服务。平台既不启动它也不代理它；平台只决定作业能不能够到那台主机，
    然后把 URL 交给 trainer。代价只是一个出网决策。

    ```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
    sf env runner register http://envbox-3.corp:8080 \
      --name shared-sim --version 1.0.0 \
      --rubric alice/task-completion
    ```

    这条命令会验证正在运行的服务并注册它，不需要管理员介入。
  </Tab>

  <Tab title="openenv-image —— 这里跑不了">
    manifest 解析器接受它，但**准入时会拒绝**。在作业旁边托管别人的容器是执行器的活，
    这套部署不做。拒绝发生在提交时而不是 launcher 里，所以你在花掉一次资源分配之前就知道。
  </Tab>
</Tabs>

## Harness

驱动模型走完环境的那段代码，以 `kind: environment` 插件提供。

**物化**那条路需要它——框架要 import 并运行它。
**服务**那条回合级路径不需要——trainer 自己驱动 episode。

```yaml plugin.yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
schema: forge/plugin/v1
name: calculator-harness
version: 1.0.0
kind: environment
entrypoint: harness:CalculatorResources
```

manifest 里的 `harness.digest` 锁定这个环境编写时所针对的 harness 包。
团队把 harness 和环境一起发布时留空是诚实的；两者分开版本化、
且 harness 版本不匹配会导致打分不同时，就把它设上。它在提交时校验，不在 rollout 时校验。

## 推送与引用

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf env push ./my-env --name calculator-tasks --version 1.0.0 --public
```

<ParamField path="--name" type="<owner>/<name> 或裸名" required>
  裸名进入你自己的命名空间。
</ParamField>

<ParamField path="--version" type="string" required>
  推送后不可变。
</ParamField>

<ParamField path="--public" type="flag">
  只在环境首次创建时生效。
</ParamField>

你笔记本上和控制平面里跑的是同一个模块里的同一份解析代码——所以能 push 的 manifest 就是能跑的 manifest。
未知的协议或 verifier kind 在 push 时就被拒，而不是等到 rollout 才发现：
一个校验通过却跑不起来的环境，已经浪费掉了某人的一次 GPU 分配。

## 确认成功

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf env show alice/calculator-tasks@1.0.0
```

打印协议、verifier、工具，以及是否需要沙箱。控制台的 Environments 页显示同样的内容，
外加哪些 run 引用过它。

## 下一步

<Columns cols={2}>
  <Card title="Verifier" icon="scale" href="/zh-Hans/extend/verifiers" arrow="true">
    判定任务完成的三种方式，以及各自的确切契约。
  </Card>

  <Card title="Rubric" icon="ruler" href="/zh-Hans/extend/rubrics" arrow="true">
    写下裁判据以打分的那份标准。
  </Card>
</Columns>
