@h-ai/runner 1.0.0-beta.24 → 1.0.0-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,79 +1,332 @@
1
1
  # @h-ai/runner
2
2
 
3
- Runner 用于运行受管的 Agent 和工具容器。API 节点提交任务和读取状态,执行节点领取任务,持有实际容器和连接。任务、归属、取消请求和事件保存在共享数据库中。应用负责身份验证和业务授权。
3
+ [简体中文](./README.md) | [English](./README.en.md) | [日本語](./README.ja.md)
4
4
 
5
- ## 启动和提交
5
+ [框架首页](../../README.md) · [关系数据库](../reldb/README.md) · [AI 智能体](../ai/README.md)
6
6
 
7
- 先初始化 `@h-ai/reldb`,再启动 runner。单机可以使用 SQLite。多实例使用共享 PostgreSQL 或 MySQL,并为每台执行节点分配稳定的 nodeId。API 节点可以设置 `executor: false`。以下示例使用已启动的数据库,仅运行可信服务器处理器。
7
+ Runner 在 Node.js 服务端运行受管的 Agent Worker 和工具作业。API 节点提交任务、查询状态和发送取消请求,执行节点领取任务并持有实际容器。任务、租约、事件和执行结果通过关系数据库共享。
8
+
9
+ ## 快速开始
10
+
11
+ ```sh
12
+ pnpm add @h-ai/runner @h-ai/reldb
13
+ ```
14
+
15
+ 先启动 RelDB,再启动 Runner。下面用 SQLite 运行一个可信的服务端处理器,无需容器引擎。把示例放在 Node.js 应用的启动脚本中运行。业务接口应从已认证身份取得 `tenantId` 和 `userId`,并在调用 Runner 前检查业务权限。
8
16
 
9
17
  ```ts
10
- import { core } from '@h-ai/core'
11
- import { runner } from '@h-ai/runner'
18
+ import { setTimeout as delay } from 'node:timers/promises' // 用于演示有界轮询。
19
+ import { reldb } from '@h-ai/reldb' // 提供 Runner 使用的关系数据库。
20
+ import { runner } from '@h-ai/runner' // 同一进程内共享的 Runner 实例。
12
21
 
13
- const scope = { tenantId: 'studio', userId: 'alice' }
14
- const started = await runner.init({ nodeId: 'worker-east' })
15
- if (!started.success)
16
- throw new Error(started.error.message)
22
+ const database = await reldb.init({ type: 'sqlite', database: ':memory:' }) // 成功为 { success: true, data: undefined }。
23
+ if (!database.success)
24
+ throw new Error(database.error.message) // 数据库不可用时中止启动。
17
25
 
18
26
  try {
19
- const registration = runner.register('report.v1', async (input, context) => {
20
- const published = await context.emit({ phase: 'ready' })
21
- if (!published.success)
22
- throw new Error(published.error.message)
23
- return { summary: input }
27
+ const started = await runner.init({ nodeId: 'worker-east' }) // 默认承担执行角色,成功后 isExecutor 为 true。
28
+ if (!started.success)
29
+ throw new Error(started.error.message) // 停止启动,不提交任务。
30
+
31
+ const registration = runner.register('report.v1', async (input, context) => { // 成功返回注销函数。
32
+ if (typeof input !== 'string')
33
+ throw new TypeError('Expected report title') // 处理器校验业务输入。
34
+ const event = await context.emit({ phase: 'ready' }) // 成功后事件序号为 1,可从另一节点读取。
35
+ if (!event.success)
36
+ throw new Error(event.error.message) // 写事件失败时结束处理器。
37
+ return { title: input } // 作为任务成功时的 output。
24
38
  })
25
39
  if (!registration.success)
26
- throw new Error(registration.error.message)
40
+ throw new Error(registration.error.message) // 重复注册会失败。
41
+
42
+ const scope = { tenantId: 'studio', userId: 'alice' } // 示例主体,生产来自服务端认证。
27
43
  const accepted = await runner.submit({
28
44
  scope,
29
45
  kind: 'report.v1',
30
- idempotencyKey: 'report-2026-11',
31
- input: 'Monthly report',
32
- })
46
+ idempotencyKey: 'report-nov',
47
+ input: 'Monthly report'
48
+ }) // 返回 queued 回执和任务 id。
33
49
  if (!accepted.success)
34
- throw new Error(accepted.error.message)
50
+ throw new Error(accepted.error.message) // 返回失败时没有可用回执。
35
51
 
36
- const task = await runner.get(scope, accepted.data.id)
37
- if (!task.success)
38
- throw new Error(task.error.message)
39
- void task.data.status
52
+ const deadline = Date.now() + 5_000 // 本示例最多等待五秒。
53
+ let completed = false // 仅在读取到成功终态时设为 true。
54
+ while (Date.now() < deadline) {
55
+ const task = await runner.get(scope, accepted.data.id) // 同一主体下读取当前状态与结果。
56
+ if (!task.success)
57
+ throw new Error(task.error.message) // 找不到或读取失败时终止示例。
58
+ if (task.data.status === 'succeeded') {
59
+ process.stdout.write(`${JSON.stringify(task.data.output)}\n`) // 输出 {"title":"Monthly report"}。
60
+ completed = true
61
+ break
62
+ }
63
+ if (task.data.status !== 'queued' && task.data.status !== 'running')
64
+ throw new Error(task.data.failure ?? task.data.status) // 任务失败与查询失败是两个独立状态。
65
+ await delay(100) // 100 ms 后继续查询,返回 undefined。
66
+ }
67
+ if (!completed)
68
+ throw new Error('Timed out waiting for the report') // 等待超时由示例报告,Runner 会在退出阶段尝试取消本节点任务。
69
+ registration.data() // 注销当前处理器,返回 undefined。
40
70
  }
41
71
  finally {
42
- const closed = await runner.close()
43
- if (!closed.success)
44
- core.logger.error('Runner shutdown failed', { code: closed.error.code })
72
+ try {
73
+ const closed = await runner.close() // 停止领取,取消本节点任务并等待清理确认。
74
+ if (!closed.success) {
75
+ process.stderr.write(`${closed.error.message}\n`) // 清理未确认时上报并保留连接供核对。
76
+ process.exitCode = 1
77
+ }
78
+ else {
79
+ const released = await reldb.close() // 释放数据库连接,成功为 { success: true, data: undefined }。
80
+ if (!released.success) {
81
+ process.stderr.write(`${released.error.message}\n`)
82
+ process.exitCode = 1
83
+ }
84
+ }
85
+ }
86
+ catch (cause) {
87
+ process.stderr.write(`${String(cause)}\n`) // 保留示例原任务的异常。
88
+ process.exitCode = 1
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## 功能
94
+
95
+ | 能力 | 用途 | 公开入口 |
96
+ | ------------ | -------------------------------------------- | -------------------------------------------------------------------- |
97
+ | 节点生命周期 | 启动 API 与执行角色,查询环境 | `init`、`close`、`isExecutor`、`environment`、`createRunner` |
98
+ | 任务与事件 | 持久提交、幂等恢复、查询、取消及分页订阅 | `register`、`submit`、`find`、`get`、`events`、`cancel` |
99
+ | 容器和文件 | 用固定镜像执行,读写标准流并取回授权产物 | `context.start`、`context.emit`、`process.wait`、`artifact`、`close` |
100
+ | 子任务与预热 | 将控制任务交给父任务节点,给工具保留容量 | `submit({ parentId })`、`registerWarmPool` |
101
+ | 未知状态核验 | 核对容器或宿主进程已停止,并记录操作员和工单 | `reconcileUnknown` |
102
+
103
+ ### 分配节点角色与读取环境
104
+
105
+ 单进程应用可使用 `runner`。`createRunner()` 返回独立实例,适用于分别注入节点角色或隔离测试。多节点生产部署使用共享 PostgreSQL 或 MySQL,同一部署使用一致的环境定义和并发上限。执行节点 ID 必须唯一,并在节点重启后保持稳定。以下示例假定 RelDB 已启动,`imageId` 由部署方提供,是每台执行节点本地已缓存的 OCI 镜像 ID 或清单 digest。
106
+
107
+ ```ts
108
+ import type { RunnerConfigInput } from '@h-ai/runner' // 用公开类型检查两类节点共用的设置。
109
+ import { createRunner, RunnerConfigSchema } from '@h-ai/runner' // 工厂返回实例,Schema 校验配置与默认值。
110
+
111
+ function prepareApi(imageId: string) {
112
+ const settings: RunnerConfigInput = {
113
+ nodeId: 'api-east', // 节点 ID,执行节点改用独立 ID。
114
+ executor: false, // API 节点提交任务、查询和取消。
115
+ environments: [
116
+ {
117
+ id: 'docs', // 提交任务时使用的 environmentId。
118
+ image: imageId, // 固定且已准备的镜像摘要。
119
+ capabilities: ['documents.extract'], // 供上层按能力选环境。
120
+ resources: {
121
+ cpu: 1,
122
+ memoryMiB: 256,
123
+ pids: 64,
124
+ scratchMiB: 64
125
+ }, // 环境允许的上限。
126
+ }
127
+ ],
128
+ }
129
+ return RunnerConfigSchema.safeParse(settings) // 成功后 data 包含 engine: 'podman' 和 offline 环境默认值。
130
+ }
131
+
132
+ async function startApi(imageId: string) {
133
+ const settings = prepareApi(imageId) // 非法摘要或资源配置会得到 Zod 错误。
134
+ if (!settings.success)
135
+ throw new Error(settings.error.message)
136
+ const api = createRunner() // 新实例,未启动时 isExecutor 为 false。
137
+ const started = await api.init(settings.data) // 成功时 data 为 undefined。
138
+ if (!started.success)
139
+ throw new Error(started.error.message)
140
+ const docs = api.environment('docs') // 成功返回配置副本,data.image 等于 imageId。
141
+ if (!docs.success)
142
+ throw new Error(docs.error.message)
143
+ return api // 应用停止时先调用并检查 api.close(),再关闭 RelDB。
45
144
  }
46
145
  ```
47
146
 
48
- `get` 读取当前状态,`find` 按主体和幂等键恢复回执,`events` 按序号分页,`cancel` 记录取消请求。只有确认容器停止且清理成功后才发布取消终态。处理器的停止信号来自持久控制状态。父任务通过 `parentId` 将子任务路由到持有连接的节点,子任务使用单独的控制并发额度。
147
+ 执行节点以 `executor: true` 启动,并通过 `register(kind, handler)` 登记可信处理器。API 节点也可以执行相同装配逻辑,其 `register` 返回空注销函数。`environment(id)` 可读当前环境,未知 ID 返回 `NOT_FOUND`。
148
+
149
+ ### 恢复任务、读取事件和取消
49
150
 
50
- ## 容器环境与资源
151
+ 同一主体、任务类型和幂等键重试相同的提交会取得原回执。改变原提交的内容会得到 `CONFLICT`。`reuseKey` 可让可信服务复用已有活跃执行,如同一用户的 Worker。幂等键可用于 `find`,复用键由服务端维护。以下函数接收已认证主体和已存在的 `report.v1` 回执对应的幂等键。
51
152
 
52
- 启动时配置 `environments`,每项提供 ID、已预拉取的镜像 digest、CPU 核数、内存 MiB、PID 数和临时盘 MiB。调用方通过 `environmentId` 选环境,并可请求更低的资源限制。执行节点在同一数据库事务内领取任务并预留资源。按主体和节点设置队列与并发上限。
153
+ ```ts
154
+ import type { RunnerScope } from '@h-ai/runner' // 主体包括 tenantId 和 userId。
155
+ import { runner } from '@h-ai/runner' // 此实例须已启动,并连接任务所在数据库。
53
156
 
54
- 为防止 Agent Worker 占满节点后工具无法运行,使用 `toolCapacity` 独立预留工具资源。默认预留 1 核 CPU、1024 MiB 内存、128 个 PID 和 512 MiB 临时盘,`toolMaxRunning` 默认为 4。宿主总预算须覆盖两个分区。可信处理器通过 `capacityClass: "tool"` 使用该分区,`concurrency: { key, limit }` 为同一类服务设置跨节点并发上限。
157
+ async function cancelReport(scope: RunnerScope, key: string) {
158
+ const found = await runner.find(scope, 'report.v1', key) // 命中返回原回执,未找到时成功且 data 为 undefined。
159
+ if (!found.success || !found.data)
160
+ return found // 保留查询错误或未命中结果。
161
+ const id = found.data.id // 后续读取、取消和分页都使用同一任务 id。
162
+ const page = await runner.events(scope, id, 0, 20) // 读取 sequence > 0 的前 20 个事件,空页为 []。
163
+ if (!page.success)
164
+ return page // 查询失败时保留原错误。
165
+ const after = page.data.at(-1)?.sequence ?? 0 // 下一页以最后一个序号为游标。
166
+ const next = await runner.events(scope, id, after, 20) // 按序读取游标之后的最多 20 项。
167
+ if (!next.success)
168
+ return next
169
+ const cancelled = await runner.cancel(scope, id) // 排队任务直接 cancelled,运行中通常先变为 cancelling。
170
+ if (!cancelled.success)
171
+ return cancelled
172
+ return runner.get(scope, id) // 重新读取当时状态,可继续轮询直至停止得到确认。
173
+ }
174
+ ```
55
175
 
56
- 处理器通过 `context.start` 启动容器,取得标准输入、输出、错误流和退出结果。镜像应支持 Linux cgroup v2,使用 `/bin/sh`,并能以 UID/GID 1000 运行。根文件系统只读。`/state`、`/workspace` 和 `/tmp` 按任务分配受限 tmpfs。启动文件复制到 `/state`。节点上已登记的内部网络必须通过引擎的 internal 检查,默认完全禁用容器网络。联网任务应使用按信任域配置的内部网关。
176
+ 取消已结束的任务会返回其当前状态。`events` 的 `after` 默认 `0`,`limit` 默认 `100`,范围 `1–100`。提交、每条事件和输出分别有 512 KiB 的 JSON 大小上限。单个执行持久化事件总量上限为 16 MiB,条数另受 `maxEvents` 限制。大文件及密钥使用业务对象引用或执行节点的密钥管理。
57
177
 
58
- 启动环境和原生连接留在执行节点。输入、事件、输出只接受有界 JSON。密钥由可信处理器在启动时解析,调用记录使用业务标识和对象引用。执行节点失联或清理无法确认时保留 `unknown` 状态和预留额度,待运维核实实际容器和外部副作用。
178
+ 执行节点按数据库时钟分批清理超过 `retentionMs` 的已结束任务、已核验未知任务和对应事件,默认保留七天。未核验的 `unknown` 及仍有关联子任务的父记录继续保留。清理后 `find` 返回空,`get` 和 `events` 返回 `NOT_FOUND`。相同幂等键此时可重新提交,调用方应在保留期内重试,并自行管理期限更长的外部幂等约束。
59
179
 
60
- 工具处理器可用 `inputs` 将规范 Base64 文件按简单文件名写入只读 `/input`,并在容器仍运行时调用 `artifact(name, maxBytes)` 从 `/output` 读取普通文件。输入总量和输出各最多 50 MiB,同时受环境临时盘限制。用于产物导出的镜像应提供 `/usr/bin/python3`。
180
+ ### 运行隔离容器、传入文件并取回产物
61
181
 
62
- 原执行节点的可信管理入口可以调用 `runner.reconcileUnknown(scope, id, { operatorId, reason })`。Runner 读取启动前保存的容器及引擎身份,确认容器已经移除并清理同一暂存目录后才释放预算和复用键,同时保存审核记录。原回执仍为 `unknown`,并增加 `reconciledAt`。缺少可信容器回执的任务继续保留预算。
182
+ 执行节点需要 Linux cgroup v2、支持 CPU/内存/PID 限额的 Podman 或 Docker,以及预先准备的固定镜像。镜像需要 `/bin/sh`,以 UID/GID 1000 运行,且不能声明额外的镜像卷。默认离线、根文件系统只读,`/state`、`/workspace` 和 `/tmp` 使用限额临时文件系统。联网环境使用 `networkPolicy: 'internal'` 和通过引擎检查的命名内部网络;部署方负责信任域与出口限制。
63
183
 
64
- 固定离线工具可在 API 与执行节点登记相同的 `registerWarmPool({ key, kind, environmentId, minReady, maxRunning, maxTotal })`。`minReady` 是全体执行节点的空容器目标,`maxTotal` 包含启动、等待、执行、清理和未核验实例。可信工具请求同时指定 `warmPool: true` 和对应服务并发键。领取前容器没有用户输入,领取成功后才写入授权文件。每个实例只执行一个作业,之后销毁。镜像需提供 `/usr/bin/python3`;预热会消耗工具预留资源。
184
+ 以下例子假定 `docs` 环境已登记,镜像另外提供 `cat` 和 `/usr/bin/python3`。处理器的示例输入是可信固定夹具。应用处理真实文件时,应在执行节点按 `context.scope` 复核授权文件 ID,并在发布产物后只返回业务产物 ID。
185
+
186
+ ```ts
187
+ import { Buffer } from 'node:buffer' // 编码输入并核对返回的文件字节。
188
+ import { once } from 'node:events' // 等待标准输出通知产物已就绪。
189
+ import { core } from '@h-ai/core' // 记录未确认的清理结果。
190
+ import { runner } from '@h-ai/runner' // 需在实际执行节点上运行。
191
+
192
+ const registration = runner.register('copy.v1', async (_input, context) => { // 返回注销函数,处理器只运行在拥有任务租约的节点。
193
+ const launched = await context.start({
194
+ command: '/bin/sh', // 固定镜像内的可信入口。
195
+ args: ['-ec', 'cat /input/source.txt > /output/result.txt; printf ready; read -r done || true'], // 文件就绪后等待 stdin 关闭。
196
+ inputs: {
197
+ 'source.txt': Buffer.from('hello').toString('base64')
198
+ }, // 只读 /input/source.txt,内容为 hello。
199
+ }) // 成功返回含 stdin、stdout、stderr、wait、artifact 和 close 的容器句柄。
200
+ if (!launched.success)
201
+ throw new Error(launched.error.message)
202
+ const process = launched.data // 在容器退出前读取产物。
203
+ try {
204
+ process.stderr.resume() // 消费错误流,避免写满后阻塞子进程。
205
+ await once(process.stdout, 'data', { signal: context.signal }) // 首次输出来自固定命令的 ready 通知。
206
+ const artifact = await process.artifact('result.txt', 64) // 成功为 { success: true, data: 'aGVsbG8=' }。
207
+ if (!artifact.success)
208
+ throw new Error(artifact.error.message)
209
+ process.stdin.end() // 关闭输入流,使示例命令退出。
210
+ const exit = await process.wait() // 本例正常退出为 { code: 0, signal: null }。
211
+ if (exit.code !== 0)
212
+ throw new Error('Copy failed')
213
+ return {
214
+ text: Buffer.from(artifact.data, 'base64').toString('utf8')
215
+ } // 示例输出 { text: 'hello' }。
216
+ }
217
+ finally {
218
+ const stopped = await process.close() // 成功时确认容器已移除,data 为 undefined。
219
+ if (!stopped.success)
220
+ core.logger.error('Container cleanup unconfirmed', { code: stopped.error.code }) // Runner 在处理器结束后再次核对清理结果。
221
+ }
222
+ })
223
+ if (!registration.success)
224
+ throw new Error(registration.error.message)
225
+ ```
226
+
227
+ 提交 `{ scope, kind: 'copy.v1', input: null, environmentId: 'docs' }` 即可排队运行。`context.start` 每个任务至多调用一次。`files` 将启动文本文件放入 `/state`,`env` 只交给容器进程,`cwd` 默认 `/workspace`。`inputs` 最多 8 个平铺文件,输入合计不超过 50 MiB 与 `scratchMiB` 中的较小值。`artifact` 读取仍在运行的容器中 `/output` 的普通文件,并受传入的 `maxBytes` 与 50 MiB 硬上限约束。应用要检查产物归属后再提供下载。
228
+
229
+ ### 路由子任务、限制工具并发和预热
230
+
231
+ 同一主体的运行中父任务可通过 `parentId` 接收子任务。子任务在同一节点及执行代次领取,适合转交已有连接的控制操作,使用 `maxControlRunning` 控制并发。`capacityClass: 'tool'` 使用独立工具资源。`concurrency: { key, limit }` 对相同任务类型和服务键设置跨节点活动上限。
232
+
233
+ ```ts
234
+ import type { RunnerScope } from '@h-ai/runner' // 由服务端认证或可信父处理器提供。
235
+ import { runner } from '@h-ai/runner' // 连接共用数据库的实例。
236
+
237
+ async function sendControl(scope: RunnerScope, parentId: string) {
238
+ return runner.submit({
239
+ scope,
240
+ parentId,
241
+ kind: 'worker.control',
242
+ input: { operation: 'status' }
243
+ }) // 返回 queued 回执;父任务已结束或不匹配时返回冲突。
244
+ }
245
+
246
+ function registerCopyPool() {
247
+ return runner.registerWarmPool({
248
+ key: 'docs-copy',
249
+ kind: 'copy.v1',
250
+ environmentId: 'docs',
251
+ minReady: 1,
252
+ maxRunning: 2,
253
+ maxTotal: 3
254
+ }) // 成功返回注销函数,执行节点须先注册 copy.v1。
255
+ }
256
+
257
+ async function queuePrewarmedCopy(scope: RunnerScope) {
258
+ return runner.submit({
259
+ scope,
260
+ kind: 'copy.v1',
261
+ input: null,
262
+ environmentId: 'docs',
263
+ capacityClass: 'tool',
264
+ warmPool: true,
265
+ concurrency: { key: 'docs-copy', limit: 2 }
266
+ }) // 返回 queued 回执,执行节点分配一次性容器。
267
+ }
268
+ ```
269
+
270
+ `worker.control` 由应用在执行节点注册。启动时调用一次 `registerCopyPool()` 并检查 `success`,API 与执行节点使用相同值。预热容器只处理一次任务,领取后才传入用户文件。`maxTotal` 包含准备、待命、运行、清理及未核验实例,`maxRunning` 限制活动作业。预热需要 `/usr/bin/python3` 和离线环境,启动作业必须提供 `inputs`,工作目录使用 `/workspace`,且 `env`、`files` 为空。应用停止接受相应工具后,可调用登记回执中的注销函数,释放本节点未领取的预热容器。
271
+
272
+ ### 核验未知执行
273
+
274
+ 失租或清理失败可能将任务标为 `unknown`,并保留资源占用。运维先确认实际容器和外部副作用,再在原执行节点调用管理入口。Runner 校验引擎身份、实际容器已移除及本地暂存已清理,保留审计记录。
275
+
276
+ 没有容器的可信宿主任务在运维确认应用侧资源均已停止后传入 `hostResourcesStopped: true`。Runner 在同一 Linux 宿主、系统启动代次和 PID 命名空间中核验旧执行进程已退出。控制子任务还要求父执行已结束或已经核验。旧记录缺少进程证明,或者预定使用容器的任务缺少容器凭据时,继续保留占用。
277
+
278
+ ```ts
279
+ import type { RunnerScope } from '@h-ai/runner' // 从任务归属和可信管理会话取得。
280
+ import { runner } from '@h-ai/runner' // 此处必须是原执行节点上的实例。
281
+
282
+ async function reconcile(scope: RunnerScope, id: string) {
283
+ const checked = await runner.reconcileUnknown(scope, id, { operatorId: 'ops-1', reason: 'INC-47: container cleanup verified' }) // 核验成功返回 unknown 回执和 reconciledAt。
284
+ if (!checked.success)
285
+ return checked // 缺少可信容器回执、仍在运行或引擎不匹配时保留原错误与占用。
286
+ return checked // 可释放执行预算和复用键,外部业务结果仍待业务侧核对。
287
+ }
288
+
289
+ async function reconcileHost(scope: RunnerScope, id: string) {
290
+ const checked = await runner.reconcileUnknown(scope, id, { operatorId: 'ops-1', reason: 'INC-48: host resources verified', hostResourcesStopped: true }) // 输入可信操作员核对结论,成功返回带 reconciledAt 的 unknown 回执。
291
+ if (!checked.success)
292
+ return checked // 旧进程尚存、父任务未核验或缺少本机证明时保留占用。
293
+ return checked // 仅确认资源释放,业务结果仍需核对。
294
+ }
295
+ ```
296
+
297
+ ## 配置
298
+
299
+ `runner.init(config)` 接收对象。由宿主读取部署配置并明确传入。`RunnerConfigSchema.safeParse(config)` 可提前验证输入。以下时间单位均为毫秒,节点常规和工具预算需要分别核算,宿主需要能承载两者之和。
300
+
301
+ | 字段 | 默认值 | 用途 |
302
+ | ------------------------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
303
+ | `nodeId` | 必填 | 节点身份,重启后保持稳定 |
304
+ | `executor`、`engine` | `true`、`podman` | 执行角色与容器引擎,亦支持 `docker` |
305
+ | `capacity` | `4 CPU / 4096 MiB / 512 PID / 2048 MiB` | 普通执行资源,依次为 CPU、内存、PID、临时盘 |
306
+ | `toolCapacity`、`toolMaxRunning` | `1 CPU / 1024 MiB / 128 PID / 512 MiB`、`4` | 工具的独立节点预算与并发 |
307
+ | `maxRunning`、`maxRunningPerScope`、`maxControlRunning` | `8`、`4`、`8` | 节点普通并发、主体并发、节点控制子任务并发 |
308
+ | `maxQueued`、`maxEvents` | `5000`、`10000` | 常规和控制队列分别限长,每个执行限制持久事件条数 |
309
+ | `retentionMs` | `604800000` | 终态记录和事件保留期,范围 1 小时至 365 天,同一部署的节点使用相同设置 |
310
+ | `leaseMs`、`pollMs`、`closeTimeoutMs` | `10000`、`50`、`20000` | 租约、轮询和等待关闭,`pollMs × 3` 必须小于 `leaseMs` |
311
+ | `environments` | `[]` | 环境 ID、固定镜像、资源、可选 `architecture` 和 `capabilities`。网络默认 `offline`,`internal` 需指定 `network` |
312
+
313
+ 任务可用 `resources` 调低环境上限。`queueTimeoutMs` 和 `runTimeoutMs` 默认均为 `300000`。可信服务可用 `nodeId` 固定节点,或者用 `parentId` 绑定活动父任务。工具池使用登记环境的资源值。
314
+
315
+ ## 错误处理
316
+
317
+ 先检查 `result.success`,再读 `data` 或 `error`。初始化、无效配置、不存在、冲突、容量不足、失租和清理未确认的错误由 `HaiRunnerError` 定义。处理器失败则写入执行状态,可通过成功的 `get()` 读取 `status: 'failed'` 及 `failure`。`cancel()` 成功表示取消请求已受理,停止结果由状态确认。`close()` 返回清理未确认时,应保留数据库连接以便检查或重试关闭。
65
318
 
66
319
  ## 开发测试
67
320
 
68
- ```bash
321
+ 在仓库根目录执行(PowerShell 使用 `pnpm.cmd`)。SQLite 验证任务流程,PostgreSQL/MySQL 验证跨节点归属,Podman/Docker 验证真实容器限制、取消和产物。`HAI_TEST_CONTAINER_ENGINE` 可选择容器引擎;数据库测试还需可用的 Docker 兼容 Testcontainers 接口。`HAI_TEST_RUNNER_ARTIFACT_IMAGE` 指定预先构建的测试工具镜像,启用产物和跨节点预热测试。
322
+
323
+ ```sh
69
324
  pnpm --filter @h-ai/runner typecheck
70
325
  pnpm --filter @h-ai/runner lint
71
326
  pnpm --filter @h-ai/runner build
72
327
  pnpm --filter @h-ai/runner test
73
328
  ```
74
329
 
75
- 测试包含真实 SQLite、PostgreSQL、MySQL 及 Docker/Podman 容器。通过 `HAI_TEST_CONTAINER_ENGINE` 指定容器引擎。PostgreSQL 与 MySQL 测试通过 Testcontainers 连接同一引擎提供的 Docker 兼容 API。
76
-
77
330
  ## 许可证
78
331
 
79
332
  Apache-2.0
package/dist/index.d.ts CHANGED
@@ -83,6 +83,7 @@ declare const RunnerConfigSchema: z.ZodObject<{
83
83
  maxControlRunning: z.ZodDefault<z.ZodNumber>;
84
84
  maxQueued: z.ZodDefault<z.ZodNumber>;
85
85
  maxEvents: z.ZodDefault<z.ZodNumber>;
86
+ retentionMs: z.ZodDefault<z.ZodNumber>;
86
87
  closeTimeoutMs: z.ZodDefault<z.ZodNumber>;
87
88
  environments: z.ZodDefault<z.ZodArray<z.ZodObject<{
88
89
  id: z.ZodString;
@@ -250,6 +251,8 @@ interface RunnerExecution {
250
251
  interface RunnerReconciliation {
251
252
  operatorId: string;
252
253
  reason: string;
254
+ /** 核对宿主处理器的所有应用侧资源均已停止。没有容器凭据时必须提供。 */
255
+ hostResourcesStopped?: true;
253
256
  }
254
257
  /**
255
258
  * 已登记离线固定工具的集群预热目标及容量边界。
@@ -372,7 +375,7 @@ interface RunnerFunctions {
372
375
  get: (scope: RunnerScope, id: string) => Promise<HaiResult<RunnerExecution>>;
373
376
  /** 持久记录取消请求。 */
374
377
  cancel: (scope: RunnerScope, id: string) => Promise<HaiResult<RunnerExecution>>;
375
- /** 原执行节点管理入口;核验环境已释放并审计,仍保留未知业务结果。 */
378
+ /** 原执行节点管理入口;核验环境或原宿主进程已释放并审计,仍保留未知业务结果。 */
376
379
  reconcileUnknown: (scope: RunnerScope, id: string, review: RunnerReconciliation) => Promise<HaiResult<RunnerExecution>>;
377
380
  /** 分页读取保序事件。 */
378
381
  events: (scope: RunnerScope, id: string, after?: number, limit?: number) => Promise<HaiResult<RunnerEvent[]>>;