@yss123/cage-ai 0.0.0-stage → 0.3.0
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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/budget.d.ts +64 -0
- package/dist/budget.js +54 -0
- package/dist/budget.js.map +1 -0
- package/dist/context-read.d.ts +38 -0
- package/dist/context-read.js +124 -0
- package/dist/context-read.js.map +1 -0
- package/dist/context-schema.d.ts +9 -0
- package/dist/context-schema.js +16 -0
- package/dist/context-schema.js.map +1 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +83 -0
- package/dist/context.js.map +1 -0
- package/dist/execution-schema.d.ts +9 -0
- package/dist/execution-schema.js +20 -0
- package/dist/execution-schema.js.map +1 -0
- package/dist/history.d.ts +43 -0
- package/dist/history.js +139 -0
- package/dist/history.js.map +1 -0
- package/dist/http.d.ts +15 -0
- package/dist/http.js +197 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/model-failure.d.ts +66 -0
- package/dist/model-failure.js +85 -0
- package/dist/model-failure.js.map +1 -0
- package/dist/model.d.ts +50 -0
- package/dist/model.js +93 -0
- package/dist/model.js.map +1 -0
- package/dist/models.d.ts +75 -0
- package/dist/models.js +241 -0
- package/dist/models.js.map +1 -0
- package/dist/personalities.d.ts +87 -0
- package/dist/personalities.js +191 -0
- package/dist/personalities.js.map +1 -0
- package/dist/personality-presets.d.ts +14 -0
- package/dist/personality-presets.js +24 -0
- package/dist/personality-presets.js.map +1 -0
- package/dist/prompt.d.ts +67 -0
- package/dist/prompt.js +148 -0
- package/dist/prompt.js.map +1 -0
- package/dist/query.d.ts +13 -0
- package/dist/query.js +82 -0
- package/dist/query.js.map +1 -0
- package/dist/request-plan.d.ts +59 -0
- package/dist/request-plan.js +168 -0
- package/dist/request-plan.js.map +1 -0
- package/dist/responses.d.ts +29 -0
- package/dist/responses.js +134 -0
- package/dist/responses.js.map +1 -0
- package/dist/runtime.d.ts +172 -0
- package/dist/runtime.js +1188 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema.d.ts +11 -0
- package/dist/schema.js +51 -0
- package/dist/schema.js.map +1 -0
- package/dist/security.d.ts +23 -0
- package/dist/security.js +73 -0
- package/dist/security.js.map +1 -0
- package/dist/state.d.ts +238 -0
- package/dist/state.js +11 -0
- package/dist/state.js.map +1 -0
- package/dist/store.d.ts +113 -0
- package/dist/store.js +456 -0
- package/dist/store.js.map +1 -0
- package/dist/text-stream.d.ts +62 -0
- package/dist/text-stream.js +99 -0
- package/dist/text-stream.js.map +1 -0
- package/dist/tool-catalog.d.ts +23 -0
- package/dist/tool-catalog.js +53 -0
- package/dist/tool-catalog.js.map +1 -0
- package/dist/tools.d.ts +54 -0
- package/dist/tools.js +250 -0
- package/dist/tools.js.map +1 -0
- package/dist/types.d.ts +283 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -3
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 执行可恢复的 AI 状态机:准备上下文、调用模型、逐个工具动作、保存观察并继续推理。
|
|
3
|
+
* worker 通过 tick 领取任务,数据库保存阶段、预算、动作与回执;进程只持有短期租约。模型网络调用在事务外,业务写与动作回执在同一事务,租约失效后旧执行者不得提交。
|
|
4
|
+
* 审批和助理共用执行机制但使用不同身份、工具与结论边界;停止、取消、超时和远端历史恢复分别处理,不把模型文字当作业务执行成功。
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { cageError, isCageError } from '@yss123/cage-kernel';
|
|
8
|
+
import { runtimeOntology } from '@yss123/cage-ontology';
|
|
9
|
+
import { aiDecisionSchema, personalityPreviewSchema, } from '@yss123/cage-ai-protocol';
|
|
10
|
+
import { sql } from 'kysely';
|
|
11
|
+
import { AiStore, event, json } from './store.js';
|
|
12
|
+
import { authFingerprint, contextForRun, digest, parse } from './security.js';
|
|
13
|
+
import { pageContext } from './context.js';
|
|
14
|
+
import { checkedTool, engines, toolsFor } from './tools.js';
|
|
15
|
+
import { DEFAULT_LIMITS, } from './types.js';
|
|
16
|
+
import { validateModelResult } from './model.js';
|
|
17
|
+
import { mountHttp, aiOpenApi } from './http.js';
|
|
18
|
+
import { ModelManager } from './models.js';
|
|
19
|
+
import { TextStream } from './text-stream.js';
|
|
20
|
+
import { personalityPrompt } from './personalities.js';
|
|
21
|
+
import { contextPreview } from './prompt.js';
|
|
22
|
+
import { ResponseHistoryExpired } from './responses.js';
|
|
23
|
+
import { modelBudget, reservedTokens } from './budget.js';
|
|
24
|
+
import { loadConversation } from './history.js';
|
|
25
|
+
import { planModelRequest, discoveredToolKeys } from './request-plan.js';
|
|
26
|
+
import { modelFailure } from './model-failure.js';
|
|
27
|
+
/**
|
|
28
|
+
* 内部控制异常,表示运行已取消、租约过期或被其他执行者接管。
|
|
29
|
+
* 旧 worker 遇到它安静退出,不覆盖新执行者状态,也不把失去租约记成新的业务故障。
|
|
30
|
+
*/
|
|
31
|
+
class LostLease extends Error {
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 运行租约有效期,单位毫秒;执行循环每 5 秒续租,所有状态写入还需在事务内重新核验租约。
|
|
35
|
+
*/
|
|
36
|
+
const LEASE_MS = 30_000;
|
|
37
|
+
/**
|
|
38
|
+
* 以取消信号和异步操作竞争,取消时及时结束调用方等待并移除监听器。
|
|
39
|
+
* 底层操作仍必须自行响应传入信号;Promise.race 不能强制终止不配合取消的外部实现,后续提交仍依赖租约校验阻止迟到结果。
|
|
40
|
+
*/
|
|
41
|
+
async function abortable(signal, fn) {
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
let abort;
|
|
44
|
+
const cancelled = new Promise((_resolve, reject) => {
|
|
45
|
+
abort = () => reject(signal.reason);
|
|
46
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([Promise.resolve().then(fn), cancelled]);
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
signal.removeEventListener('abort', abort);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** context → model → actions → observation 的可恢复状态机。数据库是状态源,进程只持有带期限的租约。 */
|
|
56
|
+
/**
|
|
57
|
+
* 持久化 AI 运行服务,继承用户会话存储并接入 HTTP、OpenAPI 和 worker 生命周期。
|
|
58
|
+
* 以租约行锁保护状态推进,以动作回执保护业务写恢复;模型调用失败时保守计入未知用量,工具读取可能重试,已成功相同写意图优先复用回执。
|
|
59
|
+
*/
|
|
60
|
+
export class AiRuntime extends AiStore {
|
|
61
|
+
/**
|
|
62
|
+
* 与当前注册表指纹一致的运行时本体,用于工具发现和上下文说明。
|
|
63
|
+
*/
|
|
64
|
+
ontology;
|
|
65
|
+
/**
|
|
66
|
+
* 租户动态模型管理入口,与静态模型 provider 共存。
|
|
67
|
+
*/
|
|
68
|
+
models;
|
|
69
|
+
/**
|
|
70
|
+
* 整个 runtime 的关闭信号,传播到模型调用和执行循环。
|
|
71
|
+
*/
|
|
72
|
+
stopping = new AbortController();
|
|
73
|
+
/**
|
|
74
|
+
* 当前进程实际执行中的运行 Promise 集合,用于并发限制和关闭等待。
|
|
75
|
+
*/
|
|
76
|
+
active = new Set();
|
|
77
|
+
/**
|
|
78
|
+
* 正在处理的一次 worker tick,用于合并重入调用。
|
|
79
|
+
*/
|
|
80
|
+
tickPromise = null;
|
|
81
|
+
/**
|
|
82
|
+
* 正在试聊的租户/用户组合,限制单用户和本进程并发,不是跨进程全局配额。
|
|
83
|
+
*/
|
|
84
|
+
previewing = new Set();
|
|
85
|
+
/**
|
|
86
|
+
* 构建运行时本体与注册表指纹,绑定 managed provider 并收集可管理模型别名。
|
|
87
|
+
* 外部提供的本体指纹必须与当前注册表一致,否则启动失败;构造不调用模型或执行任务。
|
|
88
|
+
*/
|
|
89
|
+
constructor(kernel, options) {
|
|
90
|
+
const ontology = runtimeOntology(kernel.registry);
|
|
91
|
+
super(kernel, {
|
|
92
|
+
...options,
|
|
93
|
+
models: Object.fromEntries(Object.entries(options.models).map(([name, provider]) => [
|
|
94
|
+
name,
|
|
95
|
+
provider.bind?.(kernel) ?? provider,
|
|
96
|
+
])),
|
|
97
|
+
}, ontology.fingerprint);
|
|
98
|
+
this.models = new ModelManager(kernel, [
|
|
99
|
+
...new Set(Object.values(options.models)
|
|
100
|
+
.filter((p) => p.spec.kind === 'managed-model')
|
|
101
|
+
.map((p) => String(p.spec.key))),
|
|
102
|
+
]);
|
|
103
|
+
if (options.ontology && options.ontology.fingerprint !== ontology.fingerprint)
|
|
104
|
+
throw new Error('AI 本体清单与运行注册表不一致');
|
|
105
|
+
this.ontology = options.ontology ?? ontology;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* 用零行查询确认运行、模型、性格及后续状态字段迁移已经可读。
|
|
109
|
+
* 只检查数据库结构依赖,不代表外部模型连通、业务授权或实际 AI 流程已验证。
|
|
110
|
+
*/
|
|
111
|
+
async ready() {
|
|
112
|
+
await this.kernel.app
|
|
113
|
+
.selectFrom('sys.ai_run')
|
|
114
|
+
.select(['id', 'model_reservation', 'tool_plan'])
|
|
115
|
+
.limit(0)
|
|
116
|
+
.execute();
|
|
117
|
+
await this.kernel.app.selectFrom('sys.ai_model').select('key').limit(0).execute();
|
|
118
|
+
await this.kernel.app.selectFrom('sys.ai_personality').select('key').limit(0).execute();
|
|
119
|
+
await this.kernel.app.selectFrom('sys.ai_run').select('model_state').limit(0).execute();
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* 发出进程停止信号并等待当前活跃执行结束;执行循环尝试释放租约并重新排队,不能撤销先前已提交的领域命令。
|
|
123
|
+
*/
|
|
124
|
+
async close() {
|
|
125
|
+
this.stopping.abort();
|
|
126
|
+
await Promise.allSettled([...this.active]);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* 把固定 AI HTTP 接口挂载到宿主服务;认证、SSE 和归属边界由 mountHttp 与存储层共同执行。
|
|
130
|
+
*/
|
|
131
|
+
http(app) {
|
|
132
|
+
mountHttp(app, this);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* 返回 AI 固定接口的 OpenAPI 路径片段,供宿主统一合并,不在此注册路由。
|
|
136
|
+
*/
|
|
137
|
+
openapi() {
|
|
138
|
+
return aiOpenApi();
|
|
139
|
+
}
|
|
140
|
+
/** 性格试聊只请求一次模型,不提供业务工具、页面、记忆或用户历史。草稿无需先保存。 */
|
|
141
|
+
/**
|
|
142
|
+
* 在性格管理授权下对未保存草稿进行一次纯文本试聊。
|
|
143
|
+
* 每个用户同时最多一次、当前 runtime 最多四次;20 秒中止、最多 600 输出 Token,不提供工具、页面、偏好或会话历史。
|
|
144
|
+
* 开始前记录管理操作审计,拒绝任何工具调用结果,finally 等待流清理并释放并发名额;不创建正式助理运行。
|
|
145
|
+
*/
|
|
146
|
+
async previewPersonality(ctx, raw, onText, signal) {
|
|
147
|
+
this.personalities.need(ctx);
|
|
148
|
+
const input = parse(personalityPreviewSchema, raw), model = this.options.assistant?.model;
|
|
149
|
+
if (!model)
|
|
150
|
+
throw cageError('NOT_FOUND', '应用未启用业务助理');
|
|
151
|
+
const owner = `${ctx.tenantId}:${ctx.userId}`;
|
|
152
|
+
if (this.previewing.has(owner) || this.previewing.size >= 4)
|
|
153
|
+
throw cageError('RATE_LIMITED');
|
|
154
|
+
this.previewing.add(owner);
|
|
155
|
+
const cancel = AbortSignal.any([
|
|
156
|
+
this.stopping.signal,
|
|
157
|
+
AbortSignal.timeout(20_000),
|
|
158
|
+
...(signal ? [signal] : []),
|
|
159
|
+
]);
|
|
160
|
+
const stream = new TextStream(async (text) => {
|
|
161
|
+
if (onText)
|
|
162
|
+
await onText(text);
|
|
163
|
+
}, cancel), started = Date.now();
|
|
164
|
+
try {
|
|
165
|
+
await this.personalities.audit(this.kernel.app, ctx, 'preview', { model, label: input.profile.label });
|
|
166
|
+
const result = validateModelResult(await abortable(cancel, () => this.options.models[model].generate({
|
|
167
|
+
messages: [
|
|
168
|
+
{
|
|
169
|
+
role: 'system',
|
|
170
|
+
content: `这是助理性格试聊。请用纯文本短段落展示表达方式;未连接业务工具,不能查询或修改真实业务。不要假装执行过任何操作。\n${personalityPrompt(input.profile)}`,
|
|
171
|
+
},
|
|
172
|
+
{ role: 'user', content: input.message },
|
|
173
|
+
],
|
|
174
|
+
tools: [],
|
|
175
|
+
maxOutputTokens: 600,
|
|
176
|
+
context: { tenantId: ctx.tenantId, runId: randomUUID() },
|
|
177
|
+
signal: cancel,
|
|
178
|
+
onText: (text) => stream.push(text),
|
|
179
|
+
})));
|
|
180
|
+
if (result.calls.length)
|
|
181
|
+
throw cageError('INVALID_INPUT', '试聊只支持文字回复');
|
|
182
|
+
await stream.finish();
|
|
183
|
+
return { text: result.text, latencyMs: Date.now() - started };
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
await stream.dispose();
|
|
187
|
+
this.previewing.delete(owner);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* 在调用方事务中锁定运行行,检查 token、running 状态、取消标记和未到期租约。
|
|
192
|
+
* 返回锁下最新快照供变更使用;任何不满足均抛 LostLease,避免旧内存快照授权迟到提交。
|
|
193
|
+
*/
|
|
194
|
+
async guard(tx, id, token) {
|
|
195
|
+
const run = (await tx
|
|
196
|
+
.selectFrom('sys.ai_run')
|
|
197
|
+
.selectAll()
|
|
198
|
+
.where('id', '=', id)
|
|
199
|
+
.forUpdate()
|
|
200
|
+
.executeTakeFirstOrThrow());
|
|
201
|
+
if (!token ||
|
|
202
|
+
run.lease_token !== token ||
|
|
203
|
+
run.status !== 'running' ||
|
|
204
|
+
run.cancelled ||
|
|
205
|
+
!run.lease_until ||
|
|
206
|
+
new Date(run.lease_until).getTime() <= Date.now())
|
|
207
|
+
throw new LostLease();
|
|
208
|
+
return run;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 为一次运行状态变更打开事务并先执行 guard,再把锁下最新状态传给回调。
|
|
212
|
+
* 状态、动作和事件可由回调原子提交;不应把外部模型网络请求放在这个事务内。
|
|
213
|
+
*/
|
|
214
|
+
async change(run, fn) {
|
|
215
|
+
await this.kernel.app.transaction().execute(async (tx) => {
|
|
216
|
+
const current = await this.guard(tx, run.id, run.lease_token);
|
|
217
|
+
await fn(tx, current);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* 内部读取完整运行行供执行循环恢复阶段;不执行用户归属授权。
|
|
222
|
+
* 公开访问必须走 ownedRun,内部使用后在执行前调用 identity 并在写入前 guard。
|
|
223
|
+
*/
|
|
224
|
+
async readRun(id) {
|
|
225
|
+
return this.kernel.app
|
|
226
|
+
.selectFrom('sys.ai_run')
|
|
227
|
+
.selectAll()
|
|
228
|
+
.where('id', '=', id)
|
|
229
|
+
.executeTakeFirstOrThrow();
|
|
230
|
+
}
|
|
231
|
+
/** 与审批步骤关联一次;旧轮次取消后不会被再次领取。 */
|
|
232
|
+
/**
|
|
233
|
+
* 领取最多 20 个仍运行实例中的未绑定 AI 步骤,以行锁和 skipLocked 避免多 worker 重复建运行。
|
|
234
|
+
* 创建审批 run、绑定 step.run_id、切换步骤状态及首个事件同事务完成;只处理 queued 且未关联的步骤。
|
|
235
|
+
*/
|
|
236
|
+
async enqueueApprovals() {
|
|
237
|
+
if (!engines({ kernel: this.kernel }).length)
|
|
238
|
+
return 0;
|
|
239
|
+
return this.kernel.app.transaction().execute(async (tx) => {
|
|
240
|
+
const rows = await tx
|
|
241
|
+
.selectFrom('flow.approval_step as s')
|
|
242
|
+
.innerJoin('flow.approval_instance as i', 'i.id', 's.instance_id')
|
|
243
|
+
.select(['s.id', 's.step', 's.tenant_id', 'i.process', 'i.steps'])
|
|
244
|
+
.where('s.kind', '=', 'ai')
|
|
245
|
+
.where('s.status', '=', 'queued')
|
|
246
|
+
.where('s.run_id', 'is', null)
|
|
247
|
+
.where('i.status', '=', 'running')
|
|
248
|
+
.orderBy('s.created_at')
|
|
249
|
+
.limit(20)
|
|
250
|
+
.forUpdate('s')
|
|
251
|
+
.skipLocked()
|
|
252
|
+
.execute();
|
|
253
|
+
for (const row of rows) {
|
|
254
|
+
const policy = row.steps[row.step], id = randomUUID();
|
|
255
|
+
await tx
|
|
256
|
+
.insertInto('sys.ai_run')
|
|
257
|
+
.values({
|
|
258
|
+
id,
|
|
259
|
+
tenant_id: row.tenant_id,
|
|
260
|
+
mode: 'approval',
|
|
261
|
+
status: 'queued',
|
|
262
|
+
approval_step_id: row.id,
|
|
263
|
+
process: row.process,
|
|
264
|
+
policy: json(policy),
|
|
265
|
+
model: policy.model,
|
|
266
|
+
instructions: policy.instructions,
|
|
267
|
+
limits: json({ ...DEFAULT_LIMITS, maxSteps: 5, maxToolCalls: 8, ...policy.limits }),
|
|
268
|
+
fingerprint: this.fingerprint,
|
|
269
|
+
delegated_until: new Date(Date.now() + (this.options.delegationTtlMs ?? 30 * 60_000)),
|
|
270
|
+
})
|
|
271
|
+
.execute();
|
|
272
|
+
await tx
|
|
273
|
+
.updateTable('flow.approval_step')
|
|
274
|
+
.set({ run_id: id, status: 'running' })
|
|
275
|
+
.where('id', '=', row.id)
|
|
276
|
+
.execute();
|
|
277
|
+
await event(tx, id, 'run.status', { status: 'queued' });
|
|
278
|
+
}
|
|
279
|
+
return rows.length;
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* 按创建顺序领取 queued 或租约已过期的 running 任务。
|
|
284
|
+
* 同会话较早非终态运行阻止后续轮次,保证用户上下文顺序;skipLocked 避免争抢阻塞。
|
|
285
|
+
* 生成新租约 token,保留已有执行 deadline 或首次按 timeoutMs 创建,并同事务写 running 事件;没有可领取任务返回 null。
|
|
286
|
+
*/
|
|
287
|
+
async claim() {
|
|
288
|
+
return this.kernel.app.transaction().execute(async (tx) => {
|
|
289
|
+
const run = await tx
|
|
290
|
+
.selectFrom('sys.ai_run as r')
|
|
291
|
+
.selectAll('r')
|
|
292
|
+
.where((eb) => eb.or([
|
|
293
|
+
eb('r.status', '=', 'queued'),
|
|
294
|
+
eb.and([eb('r.status', '=', 'running'), eb('r.lease_until', '<', new Date())]),
|
|
295
|
+
]))
|
|
296
|
+
.where(sql `not exists (select 1 from sys.ai_run earlier where earlier.session_id = r.session_id
|
|
297
|
+
and (earlier.created_at, earlier.id) < (r.created_at, r.id) and earlier.status not in ('succeeded','failed','cancelled','expired'))`)
|
|
298
|
+
.orderBy('r.created_at')
|
|
299
|
+
.orderBy('r.id')
|
|
300
|
+
.limit(1)
|
|
301
|
+
.forUpdate()
|
|
302
|
+
.skipLocked()
|
|
303
|
+
.executeTakeFirst();
|
|
304
|
+
if (!run)
|
|
305
|
+
return null;
|
|
306
|
+
const token = randomUUID(), deadline = run.deadline ?? new Date(Date.now() + run.limits.timeoutMs);
|
|
307
|
+
await tx
|
|
308
|
+
.updateTable('sys.ai_run')
|
|
309
|
+
.set({
|
|
310
|
+
status: 'running',
|
|
311
|
+
lease_token: token,
|
|
312
|
+
lease_until: new Date(Date.now() + LEASE_MS),
|
|
313
|
+
deadline,
|
|
314
|
+
})
|
|
315
|
+
.where('id', '=', run.id)
|
|
316
|
+
.execute();
|
|
317
|
+
await event(tx, run.id, 'run.status', { status: 'running' });
|
|
318
|
+
return { ...run, status: 'running', lease_token: token, deadline };
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* 合并同一 runtime 上重叠的 worker tick 调用,返回同一工作 Promise。
|
|
323
|
+
* 停止后返回 0;本轮完成才清空缓存,避免并发扫描导致超过当前进程并发名额。
|
|
324
|
+
*/
|
|
325
|
+
tick(signal) {
|
|
326
|
+
if (this.stopping.signal.aborted)
|
|
327
|
+
return Promise.resolve(0);
|
|
328
|
+
if (this.tickPromise)
|
|
329
|
+
return this.tickPromise;
|
|
330
|
+
this.tickPromise = this.work(signal).finally(() => {
|
|
331
|
+
this.tickPromise = null;
|
|
332
|
+
});
|
|
333
|
+
return this.tickPromise;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* 先入队 AI 审批并终结委托已过期的等待互动,再按 maxConcurrentRuns(默认 2)补充执行任务。
|
|
337
|
+
* 等待用户不占活跃槽位;有空槽时短间隔补位,满槽只等待任务完成。结束前等活跃任务退出,返回本轮入队/领取计数。
|
|
338
|
+
*/
|
|
339
|
+
async work(signal) {
|
|
340
|
+
let count = await this.enqueueApprovals();
|
|
341
|
+
// 等待不占执行者;过期互动在这里终结,后续会话消息才能继续。
|
|
342
|
+
const expired = await this.kernel.app
|
|
343
|
+
.selectFrom('sys.ai_run')
|
|
344
|
+
.select('id')
|
|
345
|
+
.where('status', 'in', ['waiting_input', 'waiting_confirmation'])
|
|
346
|
+
.where('delegated_until', '<=', new Date())
|
|
347
|
+
.limit(100)
|
|
348
|
+
.execute();
|
|
349
|
+
for (const row of expired)
|
|
350
|
+
await this.kernel.app.transaction().execute(async (tx) => {
|
|
351
|
+
const r = await tx
|
|
352
|
+
.selectFrom('sys.ai_run')
|
|
353
|
+
.selectAll()
|
|
354
|
+
.where('id', '=', row.id)
|
|
355
|
+
.forUpdate()
|
|
356
|
+
.executeTakeFirstOrThrow();
|
|
357
|
+
if (!['waiting_input', 'waiting_confirmation'].includes(r.status))
|
|
358
|
+
return;
|
|
359
|
+
await tx
|
|
360
|
+
.updateTable('sys.ai_run')
|
|
361
|
+
.set({ status: 'expired', error: 'DELEGATION_EXPIRED' })
|
|
362
|
+
.where('id', '=', r.id)
|
|
363
|
+
.execute();
|
|
364
|
+
await event(tx, r.id, 'run.status', { status: 'expired' });
|
|
365
|
+
});
|
|
366
|
+
const concurrency = this.options.maxConcurrentRuns ?? 2;
|
|
367
|
+
while (!signal?.aborted && !this.stopping.signal.aborted) {
|
|
368
|
+
while (this.active.size < concurrency && !signal?.aborted && !this.stopping.signal.aborted) {
|
|
369
|
+
const run = await this.claim();
|
|
370
|
+
if (!run)
|
|
371
|
+
break;
|
|
372
|
+
count++;
|
|
373
|
+
const job = this.execute(run, signal).finally(() => this.active.delete(job));
|
|
374
|
+
this.active.add(job);
|
|
375
|
+
}
|
|
376
|
+
if (!this.active.size)
|
|
377
|
+
break;
|
|
378
|
+
// 空闲名额持续补位;全部繁忙时只等待任务完成,不扫描队列。
|
|
379
|
+
let timer;
|
|
380
|
+
const poll = new Promise((resolve) => {
|
|
381
|
+
if (this.active.size < concurrency)
|
|
382
|
+
timer = setTimeout(resolve, 100);
|
|
383
|
+
});
|
|
384
|
+
try {
|
|
385
|
+
await Promise.race([...this.active, poll]);
|
|
386
|
+
}
|
|
387
|
+
finally {
|
|
388
|
+
if (timer)
|
|
389
|
+
clearTimeout(timer);
|
|
390
|
+
}
|
|
391
|
+
if (!signal?.aborted && !this.stopping.signal.aborted)
|
|
392
|
+
count += await this.enqueueApprovals();
|
|
393
|
+
}
|
|
394
|
+
await Promise.allSettled([...this.active]);
|
|
395
|
+
return count;
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* 重新构建运行身份并核对注册表、委托及当前授权指纹。
|
|
399
|
+
* 权限或声明变化立即拒绝继续执行,旧快照不能作为持续权限许可。
|
|
400
|
+
*/
|
|
401
|
+
async identity(run) {
|
|
402
|
+
if (run.fingerprint !== this.fingerprint)
|
|
403
|
+
throw cageError('CONFLICT', '注册声明已变化');
|
|
404
|
+
const ctx = await contextForRun(this.kernel, run);
|
|
405
|
+
if (run.auth_fingerprint && run.auth_fingerprint !== authFingerprint(ctx))
|
|
406
|
+
throw cageError('FORBIDDEN', '授权已变化');
|
|
407
|
+
return ctx;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* 核对审批实例仍运行、当前步骤编号、步骤状态和 run_id 绑定。
|
|
411
|
+
* 撤回、重提或步骤推进后旧 AI 运行不得继续审查或提交结论;返回当前业务对象定位及版本。
|
|
412
|
+
*/
|
|
413
|
+
async approvalActive(run) {
|
|
414
|
+
const step = await this.kernel.app
|
|
415
|
+
.selectFrom('flow.approval_step as s')
|
|
416
|
+
.innerJoin('flow.approval_instance as i', 'i.id', 's.instance_id')
|
|
417
|
+
.select([
|
|
418
|
+
'i.aggregate',
|
|
419
|
+
'i.business_key',
|
|
420
|
+
'i.subject_version',
|
|
421
|
+
'i.status',
|
|
422
|
+
'i.current_step',
|
|
423
|
+
's.step',
|
|
424
|
+
's.status as step_status',
|
|
425
|
+
's.run_id',
|
|
426
|
+
])
|
|
427
|
+
.where('s.id', '=', run.approval_step_id)
|
|
428
|
+
.executeTakeFirst();
|
|
429
|
+
if (!step ||
|
|
430
|
+
step.status !== 'running' ||
|
|
431
|
+
step.step !== step.current_step ||
|
|
432
|
+
step.run_id !== run.id ||
|
|
433
|
+
step.step_status !== 'running')
|
|
434
|
+
throw cageError('CONFLICT', '审批步骤已失效');
|
|
435
|
+
return step;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* 解析运行使用的模型别名;动态 provider 每个模型步进读取当前租户配置。
|
|
439
|
+
* 缺少声明直接失败,不静默使用其他模型或全局租户配置。
|
|
440
|
+
*/
|
|
441
|
+
async provider(run) {
|
|
442
|
+
const configured = this.options.models[run.model];
|
|
443
|
+
if (!configured)
|
|
444
|
+
throw new Error('模型配置不存在');
|
|
445
|
+
return configured.resolve ? configured.resolve({ tenantId: run.tenant_id }) : configured;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* 为审批重新读取指定对象并核对审批启动版本,仅投影策略允许字段;为助理重新读取页面事实、本人偏好和预算内近期历史。
|
|
449
|
+
* 追加本轮任务,记录 history_start 与远端状态/省略信息,在租约事务中保存上下文并进入 model 阶段;原始页面文字不自动成为可信事实。
|
|
450
|
+
*/
|
|
451
|
+
async prepareContext(run, ctx) {
|
|
452
|
+
let context, history = [];
|
|
453
|
+
let modelState = null, disabledKey = null, omitted = false;
|
|
454
|
+
if (run.mode === 'approval') {
|
|
455
|
+
const step = await this.approvalActive(run);
|
|
456
|
+
const record = await this.kernel.read.get(step.aggregate, step.business_key, ctx);
|
|
457
|
+
if (record.version !== step.subject_version)
|
|
458
|
+
throw cageError('CONFLICT');
|
|
459
|
+
context = {
|
|
460
|
+
subject: {
|
|
461
|
+
aggregate: step.aggregate,
|
|
462
|
+
...Object.fromEntries(['id', 'status', 'version', ...run.policy.context.fields].map((f) => [f, record[f]])),
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
const provider = await this.provider(run);
|
|
468
|
+
const budget = modelBudget(provider, run.limits, run.tokens, this.options.context);
|
|
469
|
+
const prior = await loadConversation(this.kernel, ctx, run, budget.historyTokens, provider.continuation?.key);
|
|
470
|
+
history = prior.messages;
|
|
471
|
+
modelState = prior.state;
|
|
472
|
+
disabledKey = prior.disabledKey;
|
|
473
|
+
omitted = prior.omitted;
|
|
474
|
+
context = {
|
|
475
|
+
page: await pageContext(this.kernel, ctx, run.page_context),
|
|
476
|
+
preferences: await this.memories(ctx),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
const historyStart = history.length;
|
|
480
|
+
history.push({
|
|
481
|
+
role: 'user',
|
|
482
|
+
content: run.mode === 'approval'
|
|
483
|
+
? '请依据声明规则和证据审查当前对象,使用 review_decide 提交裁决。'
|
|
484
|
+
: run.input,
|
|
485
|
+
runId: run.id,
|
|
486
|
+
});
|
|
487
|
+
await this.change(run, async (tx) => {
|
|
488
|
+
await tx
|
|
489
|
+
.updateTable('sys.ai_run')
|
|
490
|
+
.set({
|
|
491
|
+
context: json(context),
|
|
492
|
+
history: json(history),
|
|
493
|
+
history_start: historyStart,
|
|
494
|
+
model_state: modelState ? json(modelState) : null,
|
|
495
|
+
history_omitted: omitted,
|
|
496
|
+
continuation_disabled_key: disabledKey,
|
|
497
|
+
tool_keys: '[]',
|
|
498
|
+
phase: 'model',
|
|
499
|
+
auth_fingerprint: authFingerprint(ctx),
|
|
500
|
+
})
|
|
501
|
+
.where('id', '=', run.id)
|
|
502
|
+
.execute();
|
|
503
|
+
await event(tx, run.id, 'context.updated', {
|
|
504
|
+
pageInstanceId: run.page_context?.pageInstanceId ?? null,
|
|
505
|
+
revision: run.page_context?.revision ?? null,
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* 以单个 provider 快照规划输入、完整工具和输出预算,先持久化尝试次数及 Token 预留再外呼。
|
|
511
|
+
* 文本通过 TextStream 在租约保护下落事件;有效结果按厂商用量或预留估算结算,保存历史、远端项和待执行调用,助理无工具时才完成。
|
|
512
|
+
* 远端历史失效且可安全恢复时清空接续状态重建本地请求;其他不确定失败保守结算预留。审批必须提交 decision,超出累计或工具预算不进入动作阶段。
|
|
513
|
+
*/
|
|
514
|
+
async model(execution) {
|
|
515
|
+
const { run } = execution;
|
|
516
|
+
const provider = await this.provider(run);
|
|
517
|
+
const plan = planModelRequest(execution, provider, this.options.context);
|
|
518
|
+
const reservation = {
|
|
519
|
+
attempt: run.model_attempts + 1,
|
|
520
|
+
step: run.steps + 1,
|
|
521
|
+
inputTokens: plan.estimatedInput,
|
|
522
|
+
outputTokens: plan.outputLimit,
|
|
523
|
+
startedAt: new Date().toISOString(),
|
|
524
|
+
};
|
|
525
|
+
// 先持久化一次调用的预留,已结算 tokens/steps 不变。崩溃恢复会保守结算未知结果。
|
|
526
|
+
await this.change(run, async (tx) => {
|
|
527
|
+
await tx
|
|
528
|
+
.updateTable('sys.ai_run')
|
|
529
|
+
.set({
|
|
530
|
+
model_attempts: reservation.attempt,
|
|
531
|
+
model_reservation: json(reservation),
|
|
532
|
+
tool_keys: json(plan.keys),
|
|
533
|
+
tool_plan: json(plan.selection),
|
|
534
|
+
model_state: plan.remote ? json(plan.remote) : null,
|
|
535
|
+
})
|
|
536
|
+
.where('id', '=', run.id)
|
|
537
|
+
.execute();
|
|
538
|
+
await event(tx, run.id, 'context.updated', {
|
|
539
|
+
estimatedInputTokens: plan.estimatedInput,
|
|
540
|
+
contextWindowTokens: plan.budget.window,
|
|
541
|
+
loadedTools: plan.tools.length,
|
|
542
|
+
toolMode: plan.selection.mode,
|
|
543
|
+
toolTokenBudget: plan.selection.tokenBudget,
|
|
544
|
+
reduced: plan.reduced,
|
|
545
|
+
remoteContinuation: !!plan.remote,
|
|
546
|
+
historyRebased: !!run.model_state && !plan.remote,
|
|
547
|
+
});
|
|
548
|
+
await event(tx, run.id, 'model.started', { step: reservation.step, attempt: reservation.attempt });
|
|
549
|
+
});
|
|
550
|
+
let firstTextMs = null;
|
|
551
|
+
const started = Date.now();
|
|
552
|
+
const stream = new TextStream((text) => this.change(run, (tx) => event(tx, run.id, 'message.delta', { step: reservation.step, text })), execution.signal);
|
|
553
|
+
let result;
|
|
554
|
+
try {
|
|
555
|
+
result = validateModelResult(await abortable(execution.signal, () => provider.generate({
|
|
556
|
+
messages: plan.messages,
|
|
557
|
+
tools: plan.tools,
|
|
558
|
+
signal: execution.signal,
|
|
559
|
+
maxOutputTokens: plan.outputLimit,
|
|
560
|
+
previousResponseId: plan.remote?.responseId,
|
|
561
|
+
stateKey: provider.continuation?.key,
|
|
562
|
+
context: { tenantId: execution.ctx.tenantId, runId: run.id },
|
|
563
|
+
onText: (text) => {
|
|
564
|
+
if (firstTextMs === null && text)
|
|
565
|
+
firstTextMs = Date.now() - started;
|
|
566
|
+
return stream.push(text);
|
|
567
|
+
},
|
|
568
|
+
})));
|
|
569
|
+
await stream.finish();
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
if (!(error instanceof ResponseHistoryExpired) || !plan.remote) {
|
|
573
|
+
const failure = modelFailure(error);
|
|
574
|
+
if (failure.code === 'AI_MODEL_TIMEOUT' && execution.signal.aborted) {
|
|
575
|
+
failure.code = 'AI_TIMEOUT';
|
|
576
|
+
failure.reason = 'run_timeout';
|
|
577
|
+
}
|
|
578
|
+
await this.settleUncertain(run, failure.reason, {
|
|
579
|
+
...failure,
|
|
580
|
+
durationMs: Date.now() - started,
|
|
581
|
+
firstTextMs,
|
|
582
|
+
});
|
|
583
|
+
if (failure.code)
|
|
584
|
+
throw new Error(failure.code);
|
|
585
|
+
throw error;
|
|
586
|
+
}
|
|
587
|
+
await this.change(run, async (tx) => {
|
|
588
|
+
const disabled = error.reason === 'unsupported';
|
|
589
|
+
await tx
|
|
590
|
+
.updateTable('sys.ai_run')
|
|
591
|
+
.set({
|
|
592
|
+
model_reservation: null,
|
|
593
|
+
model_state: null,
|
|
594
|
+
...(disabled ? { continuation_disabled_key: provider.continuation.key } : {}),
|
|
595
|
+
})
|
|
596
|
+
.where('id', '=', run.id)
|
|
597
|
+
.execute();
|
|
598
|
+
await event(tx, run.id, 'model.failed', {
|
|
599
|
+
attempt: reservation.attempt,
|
|
600
|
+
step: reservation.step,
|
|
601
|
+
reason: error.reason,
|
|
602
|
+
durationMs: Date.now() - started,
|
|
603
|
+
chargedTokens: 0,
|
|
604
|
+
retryable: true,
|
|
605
|
+
});
|
|
606
|
+
await event(tx, run.id, 'context.updated', {
|
|
607
|
+
reason: disabled ? 'remote_continuation_unsupported' : 'remote_history_expired',
|
|
608
|
+
historyRebased: true,
|
|
609
|
+
continuationDisabled: disabled,
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
finally {
|
|
615
|
+
await stream.dispose();
|
|
616
|
+
}
|
|
617
|
+
execution.signal.throwIfAborted();
|
|
618
|
+
const consumed = result.usage
|
|
619
|
+
? result.usage.inputTokens + result.usage.outputTokens
|
|
620
|
+
: reservedTokens(reservation);
|
|
621
|
+
const tokens = run.tokens + consumed;
|
|
622
|
+
const violation = tokens > run.limits.tokenBudget
|
|
623
|
+
? 'AI_BUDGET_EXCEEDED'
|
|
624
|
+
: run.tool_calls + result.calls.length > run.limits.maxToolCalls
|
|
625
|
+
? 'AI_TOOL_BUDGET_EXCEEDED'
|
|
626
|
+
: !result.calls.length && run.mode === 'approval'
|
|
627
|
+
? 'AI_DECISION_REQUIRED'
|
|
628
|
+
: null;
|
|
629
|
+
const history = [
|
|
630
|
+
...run.history,
|
|
631
|
+
{
|
|
632
|
+
role: 'assistant',
|
|
633
|
+
content: result.text,
|
|
634
|
+
runId: run.id,
|
|
635
|
+
...(result.calls.length ? { toolCalls: result.calls } : {}),
|
|
636
|
+
...(provider.continuation && result.outputItems
|
|
637
|
+
? { providerItems: { key: provider.continuation.key, items: result.outputItems } }
|
|
638
|
+
: {}),
|
|
639
|
+
},
|
|
640
|
+
];
|
|
641
|
+
const modelState = provider.continuation &&
|
|
642
|
+
result.responseId &&
|
|
643
|
+
run.continuation_disabled_key !== provider.continuation.key
|
|
644
|
+
? {
|
|
645
|
+
key: provider.continuation.key,
|
|
646
|
+
responseId: result.responseId,
|
|
647
|
+
historyLength: history.length,
|
|
648
|
+
contextTokens: consumed,
|
|
649
|
+
}
|
|
650
|
+
: null;
|
|
651
|
+
await this.change(run, async (tx) => {
|
|
652
|
+
await tx
|
|
653
|
+
.updateTable('sys.ai_run')
|
|
654
|
+
.set({
|
|
655
|
+
model_reservation: null,
|
|
656
|
+
steps: reservation.step,
|
|
657
|
+
tokens,
|
|
658
|
+
...(!violation
|
|
659
|
+
? {
|
|
660
|
+
history: json(history),
|
|
661
|
+
model_state: modelState ? json(modelState) : null,
|
|
662
|
+
pending: json(result.calls),
|
|
663
|
+
phase: result.calls.length ? 'actions' : 'done',
|
|
664
|
+
...(!result.calls.length
|
|
665
|
+
? { status: 'succeeded', answer: result.text, lease_token: null, lease_until: null }
|
|
666
|
+
: {}),
|
|
667
|
+
}
|
|
668
|
+
: {}),
|
|
669
|
+
})
|
|
670
|
+
.where('id', '=', run.id)
|
|
671
|
+
.execute();
|
|
672
|
+
await event(tx, run.id, 'model.completed', {
|
|
673
|
+
step: reservation.step,
|
|
674
|
+
attempt: reservation.attempt,
|
|
675
|
+
durationMs: Date.now() - started,
|
|
676
|
+
firstTextMs,
|
|
677
|
+
tokens: consumed,
|
|
678
|
+
usageSource: result.usage ? 'provider' : 'estimate',
|
|
679
|
+
toolCalls: result.calls.length,
|
|
680
|
+
});
|
|
681
|
+
if (!violation && !result.calls.length) {
|
|
682
|
+
await event(tx, run.id, 'message.completed', { text: result.text });
|
|
683
|
+
await event(tx, run.id, 'run.status', { status: 'succeeded' });
|
|
684
|
+
}
|
|
685
|
+
});
|
|
686
|
+
if (violation)
|
|
687
|
+
throw new Error(violation);
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* 在租约行锁下读取当前模型预留并一次性计入估算 Token,随后清空预留、写 model.failed。
|
|
691
|
+
* 网络断线或 worker 恢复无法证明厂商没有计费,因此不按零消耗处理;已清空时直接返回,避免重复结算。
|
|
692
|
+
*/
|
|
693
|
+
async settleUncertain(run, reason, failure) {
|
|
694
|
+
await this.change(run, async (tx, current) => {
|
|
695
|
+
const reservation = current.model_reservation;
|
|
696
|
+
if (!reservation)
|
|
697
|
+
return;
|
|
698
|
+
// 网络中断/进程崩溃无法证明上游未计费,保守结算预留;尝试次数独立限制恢复次数。
|
|
699
|
+
const charged = reservedTokens(reservation);
|
|
700
|
+
await tx
|
|
701
|
+
.updateTable('sys.ai_run')
|
|
702
|
+
.set({ tokens: current.tokens + charged, model_reservation: null })
|
|
703
|
+
.where('id', '=', run.id)
|
|
704
|
+
.execute();
|
|
705
|
+
await event(tx, run.id, 'model.failed', {
|
|
706
|
+
attempt: reservation.attempt,
|
|
707
|
+
step: reservation.step,
|
|
708
|
+
reason,
|
|
709
|
+
chargedTokens: charged,
|
|
710
|
+
usageSource: 'estimate',
|
|
711
|
+
retryable: false,
|
|
712
|
+
...failure,
|
|
713
|
+
});
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* 以模型步号与 call ID 建立唯一动作记录,校验输入摘要并复用已完成回执。
|
|
718
|
+
* 发现/加载把实际 keys 与回执一起保存;交互暂停并释放租约,页面回复重新安全读取;写工具默认确认,偏好写始终确认,同运行相同成功写意图即使换 call ID 也复用。
|
|
719
|
+
* 实际写入前重建身份,领域命令与动作成功回执、事件同事务提交;审批裁决要求引用已提供证据,并与流程结论和运行终态共同提交。读取动作可在回执保存前重试。
|
|
720
|
+
*/
|
|
721
|
+
async action(execution, call) {
|
|
722
|
+
const { run } = execution, hash = digest({ tool: call.name, input: call.arguments });
|
|
723
|
+
let action;
|
|
724
|
+
await this.change(run, async (tx) => {
|
|
725
|
+
const key = `${run.steps}:${call.id}`;
|
|
726
|
+
const inserted = await tx
|
|
727
|
+
.insertInto('sys.ai_action')
|
|
728
|
+
.values({
|
|
729
|
+
id: randomUUID(),
|
|
730
|
+
run_id: run.id,
|
|
731
|
+
tenant_id: run.tenant_id,
|
|
732
|
+
call_id: key,
|
|
733
|
+
tool: call.name,
|
|
734
|
+
input: json(call.arguments),
|
|
735
|
+
input_hash: hash,
|
|
736
|
+
})
|
|
737
|
+
.onConflict((oc) => oc.columns(['run_id', 'call_id']).doNothing())
|
|
738
|
+
.returning('id')
|
|
739
|
+
.executeTakeFirst();
|
|
740
|
+
if (inserted) {
|
|
741
|
+
const tool = toolsFor(execution).find((t) => t.name === call.name);
|
|
742
|
+
await event(tx, run.id, 'tool.started', {
|
|
743
|
+
callId: call.id,
|
|
744
|
+
step: run.steps,
|
|
745
|
+
tool: call.name,
|
|
746
|
+
...(/^(command|query)_/.test(call.name) && tool ? { name: tool.description.split(':')[0] } : {}),
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
action = await tx
|
|
750
|
+
.selectFrom('sys.ai_action')
|
|
751
|
+
.selectAll()
|
|
752
|
+
.where('run_id', '=', run.id)
|
|
753
|
+
.where('call_id', '=', key)
|
|
754
|
+
.executeTakeFirstOrThrow();
|
|
755
|
+
if (action.input_hash !== hash)
|
|
756
|
+
throw cageError('CONFLICT');
|
|
757
|
+
});
|
|
758
|
+
if (action.status === 'succeeded' || action.status === 'failed')
|
|
759
|
+
return this.observe(run, call, action.result, action.id);
|
|
760
|
+
let tool, args;
|
|
761
|
+
try {
|
|
762
|
+
({ tool, args } = checkedTool(toolsFor(execution), call.name, call.arguments));
|
|
763
|
+
}
|
|
764
|
+
catch (err) {
|
|
765
|
+
return this.actionError(run, action, call, err);
|
|
766
|
+
}
|
|
767
|
+
const interaction = await this.kernel.app
|
|
768
|
+
.selectFrom('sys.ai_interaction')
|
|
769
|
+
.selectAll()
|
|
770
|
+
.where('action_id', '=', action.id)
|
|
771
|
+
.executeTakeFirst();
|
|
772
|
+
// 发现动作只改变下一次模型请求的工具集合;把真正装入预算的 keys 和回执一起保存,避免恢复时声称已加载却没有 schema。
|
|
773
|
+
if (tool.effect === 'load' || tool.effect === 'discover') {
|
|
774
|
+
try {
|
|
775
|
+
const result = await tool.execute(execution, args);
|
|
776
|
+
const search = tool.effect === 'discover' ? result : null;
|
|
777
|
+
const requested = search ? search.items.map((item) => item.key) : result;
|
|
778
|
+
const keys = discoveredToolKeys(execution, requested, tool.effect === 'load');
|
|
779
|
+
return this.receipt(run, action, call, {
|
|
780
|
+
...search,
|
|
781
|
+
loaded: keys,
|
|
782
|
+
notLoaded: requested.filter((key) => !keys.includes(key)),
|
|
783
|
+
instruction: 'loaded 中的工具已加载完整参数,下一步即可调用;无需再次 tool_load。未加载项可缩小搜索或显式选择 key。',
|
|
784
|
+
}, false, requested.length ? keys : (run.tool_keys ?? []));
|
|
785
|
+
}
|
|
786
|
+
catch (error) {
|
|
787
|
+
return this.actionError(run, action, call, error);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
// 未回答时持久暂停;回答中的页面数据还要重新安全读取,用户确认或浏览器快照本身都不是数据库事实。
|
|
791
|
+
if (tool.effect === 'interaction') {
|
|
792
|
+
if (!interaction?.answered_at)
|
|
793
|
+
return this.wait(run, action, call.name === 'request_input' ? 'input' : 'page_context', args.prompt, {});
|
|
794
|
+
let result = interaction.response;
|
|
795
|
+
if (interaction.response.pageContext) {
|
|
796
|
+
const updated = await pageContext(this.kernel, execution.ctx, interaction.response.pageContext);
|
|
797
|
+
await this.change(run, async (tx) => {
|
|
798
|
+
await tx
|
|
799
|
+
.updateTable('sys.ai_run')
|
|
800
|
+
.set({
|
|
801
|
+
page_context: json(interaction.response.pageContext),
|
|
802
|
+
context: json({ ...run.context, page: updated }),
|
|
803
|
+
})
|
|
804
|
+
.where('id', '=', run.id)
|
|
805
|
+
.execute();
|
|
806
|
+
await event(tx, run.id, 'context.updated', {
|
|
807
|
+
pageInstanceId: interaction.response.pageContext.pageInstanceId,
|
|
808
|
+
revision: interaction.response.pageContext.revision,
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
result = updated;
|
|
812
|
+
}
|
|
813
|
+
return this.receipt(run, action, call, result);
|
|
814
|
+
}
|
|
815
|
+
if (tool.effect === 'write') {
|
|
816
|
+
// 同一任务、相同写意图重复出现时复用回执,即使模型换了 call ID 也不会再次提交。
|
|
817
|
+
const old = await this.kernel.app
|
|
818
|
+
.selectFrom('sys.ai_action')
|
|
819
|
+
.select('result')
|
|
820
|
+
.where('run_id', '=', run.id)
|
|
821
|
+
.where('input_hash', '=', hash)
|
|
822
|
+
.where('status', '=', 'succeeded')
|
|
823
|
+
.executeTakeFirst();
|
|
824
|
+
if (old)
|
|
825
|
+
return this.receipt(run, action, call, old.result);
|
|
826
|
+
if ((this.options.assistant?.confirmWrites !== false || call.name === 'memory_remember') &&
|
|
827
|
+
!interaction) {
|
|
828
|
+
try {
|
|
829
|
+
return this.wait(run, action, 'confirmation', '请确认以下操作', await tool.preview?.(execution, args));
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
return this.actionError(run, action, call, error);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (interaction && !interaction.answered_at)
|
|
836
|
+
return;
|
|
837
|
+
if (interaction?.response.approved === false)
|
|
838
|
+
return this.receipt(run, action, call, { cancelled: true, reason: '用户拒绝该操作' });
|
|
839
|
+
try {
|
|
840
|
+
const ctx = await this.identity(await this.readRun(run.id));
|
|
841
|
+
await this.change(run, async (tx) => {
|
|
842
|
+
await sql `select set_config('statement_timeout', '10000', true)`.execute(tx);
|
|
843
|
+
const result = await tool.execute({ ...execution, ctx }, args, tx);
|
|
844
|
+
execution.signal.throwIfAborted();
|
|
845
|
+
await this.guard(tx, run.id, run.lease_token);
|
|
846
|
+
await tx
|
|
847
|
+
.updateTable('sys.ai_action')
|
|
848
|
+
.set({ status: 'succeeded', result: json(result ?? null), updated_at: sql `now()` })
|
|
849
|
+
.where('id', '=', action.id)
|
|
850
|
+
.execute();
|
|
851
|
+
await event(tx, run.id, 'tool.completed', {
|
|
852
|
+
callId: call.id,
|
|
853
|
+
step: run.steps,
|
|
854
|
+
tool: call.name,
|
|
855
|
+
ok: true,
|
|
856
|
+
toolCalls: run.tool_calls + 1,
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
const saved = await this.kernel.app
|
|
860
|
+
.selectFrom('sys.ai_action')
|
|
861
|
+
.select('result')
|
|
862
|
+
.where('id', '=', action.id)
|
|
863
|
+
.executeTakeFirstOrThrow();
|
|
864
|
+
return this.observe(run, call, saved.result, action.id);
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
return this.actionError(run, action, call, error);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
// 裁决是审批运行专用终结动作。非 manual 必须引用已提供来源,证据引用检查不宣称能证明模型推理正确。
|
|
871
|
+
if (tool.effect === 'decision') {
|
|
872
|
+
if (run.mode !== 'approval')
|
|
873
|
+
throw new Error('AI_STATE_INVALID');
|
|
874
|
+
const decision = parse(aiDecisionSchema, args);
|
|
875
|
+
const sources = new Set([
|
|
876
|
+
'subject',
|
|
877
|
+
...run.history
|
|
878
|
+
.filter((m) => m.role === 'tool')
|
|
879
|
+
.flatMap((m) => {
|
|
880
|
+
try {
|
|
881
|
+
return [JSON.parse(m.content).source].filter(Boolean);
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
return [];
|
|
885
|
+
}
|
|
886
|
+
}),
|
|
887
|
+
]);
|
|
888
|
+
if (decision.decision !== 'manual' &&
|
|
889
|
+
(!decision.evidence.length || decision.evidence.some((e) => !sources.has(e.source))))
|
|
890
|
+
return this.actionError(run, action, call, cageError('INVALID_INPUT', '裁决必须引用已提供的证据'));
|
|
891
|
+
const engine = engines(execution).find((e) => e.route.process === run.process);
|
|
892
|
+
await this.change(run, async (tx) => {
|
|
893
|
+
await engine.completeAi(tx, run.approval_step_id, run.id, decision, execution.ctx);
|
|
894
|
+
await tx
|
|
895
|
+
.updateTable('sys.ai_action')
|
|
896
|
+
.set({ status: 'succeeded', result: json(decision) })
|
|
897
|
+
.where('id', '=', action.id)
|
|
898
|
+
.execute();
|
|
899
|
+
await tx
|
|
900
|
+
.updateTable('sys.ai_run')
|
|
901
|
+
.set({
|
|
902
|
+
status: 'succeeded',
|
|
903
|
+
phase: 'done',
|
|
904
|
+
answer: json(decision),
|
|
905
|
+
pending: '[]',
|
|
906
|
+
lease_token: null,
|
|
907
|
+
lease_until: null,
|
|
908
|
+
})
|
|
909
|
+
.where('id', '=', run.id)
|
|
910
|
+
.execute();
|
|
911
|
+
await event(tx, run.id, 'message.completed', decision);
|
|
912
|
+
await event(tx, run.id, 'run.status', { status: 'succeeded' });
|
|
913
|
+
});
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
const result = await tool.execute(execution, args);
|
|
918
|
+
execution.signal.throwIfAborted();
|
|
919
|
+
await this.receipt(run, action, call, result);
|
|
920
|
+
}
|
|
921
|
+
catch (error) {
|
|
922
|
+
await this.actionError(run, action, call, error);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* 持久化待确认、补充输入或页面更新互动,并切换等待状态、释放执行租约。
|
|
927
|
+
* 互动到期时间沿用委托期限,事件与状态同事务;等待期间不占 worker 执行槽位,回复由存储层重新排队。
|
|
928
|
+
*/
|
|
929
|
+
async wait(run, action, kind, prompt, data) {
|
|
930
|
+
await this.change(run, async (tx) => {
|
|
931
|
+
const id = randomUUID();
|
|
932
|
+
await tx
|
|
933
|
+
.insertInto('sys.ai_interaction')
|
|
934
|
+
.values({
|
|
935
|
+
id,
|
|
936
|
+
run_id: run.id,
|
|
937
|
+
action_id: action.id,
|
|
938
|
+
kind,
|
|
939
|
+
prompt,
|
|
940
|
+
data: json(data ?? {}),
|
|
941
|
+
expires_at: run.delegated_until,
|
|
942
|
+
})
|
|
943
|
+
.execute();
|
|
944
|
+
const status = kind === 'confirmation' ? 'waiting_confirmation' : 'waiting_input';
|
|
945
|
+
await tx
|
|
946
|
+
.updateTable('sys.ai_run')
|
|
947
|
+
.set({ status, lease_token: null, lease_until: null })
|
|
948
|
+
.where('id', '=', run.id)
|
|
949
|
+
.execute();
|
|
950
|
+
await event(tx, run.id, 'interaction.required', {
|
|
951
|
+
id,
|
|
952
|
+
kind,
|
|
953
|
+
prompt,
|
|
954
|
+
data,
|
|
955
|
+
expiresAt: new Date(run.delegated_until).toISOString(),
|
|
956
|
+
});
|
|
957
|
+
await event(tx, run.id, 'run.status', { status });
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* 在租约事务中保存动作成功/失败结果、可选工具选择和 tool.completed 事件,然后独立追加观察。
|
|
962
|
+
* 如果提交回执后进程退出,恢复会复用回执并继续 observe,不重复业务写;undefined 结果以 null 持久化。
|
|
963
|
+
*/
|
|
964
|
+
async receipt(run, action, call, value, failed = false, toolKeys) {
|
|
965
|
+
const result = value ?? null;
|
|
966
|
+
await this.change(run, async (tx) => {
|
|
967
|
+
await tx
|
|
968
|
+
.updateTable('sys.ai_action')
|
|
969
|
+
.set({ status: failed ? 'failed' : 'succeeded', result: json(result), updated_at: sql `now()` })
|
|
970
|
+
.where('id', '=', action.id)
|
|
971
|
+
.execute();
|
|
972
|
+
if (toolKeys)
|
|
973
|
+
await tx
|
|
974
|
+
.updateTable('sys.ai_run')
|
|
975
|
+
.set({ tool_keys: json(toolKeys) })
|
|
976
|
+
.where('id', '=', run.id)
|
|
977
|
+
.execute();
|
|
978
|
+
await event(tx, run.id, 'tool.completed', {
|
|
979
|
+
callId: call.id,
|
|
980
|
+
step: run.steps,
|
|
981
|
+
tool: call.name,
|
|
982
|
+
ok: !failed,
|
|
983
|
+
toolCalls: run.tool_calls + 1,
|
|
984
|
+
});
|
|
985
|
+
});
|
|
986
|
+
await this.observe(run, call, result, action.id);
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* 把可交给模型修正或解释的 CageError 保存为失败回执,只对 INVALID_INPUT 保留 detail。
|
|
990
|
+
* LostLease、非 Cage 异常、UNAUTHENTICATED 和 INTERNAL 继续上抛终结或中止执行;FORBIDDEN 等其他 Cage 错误成为观察,不因此授予重试绕过权限。
|
|
991
|
+
*/
|
|
992
|
+
async actionError(run, action, call, error) {
|
|
993
|
+
if (error instanceof LostLease)
|
|
994
|
+
throw error;
|
|
995
|
+
// 未认证、内部故障和非 Cage 异常终结任务;FORBIDDEN 等其余业务错误作为失败观察返回,模型不能因此绕过权限。
|
|
996
|
+
if (!isCageError(error) || ['UNAUTHENTICATED', 'INTERNAL'].includes(error.code))
|
|
997
|
+
throw error;
|
|
998
|
+
await this.receipt(run, action, call, { error: error.code, detail: error.code === 'INVALID_INPUT' ? error.detail : undefined }, true);
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* 把动作结果追加为匹配 call ID 的工具消息,移除队首 pending 并递增工具调用数。
|
|
1002
|
+
* 用户补充输入完整保留,较大证据缩为带动作引用的预览;使用锁下当前历史避免覆盖前一次状态,剩余调用继续 actions,否则回到 model。
|
|
1003
|
+
*/
|
|
1004
|
+
async observe(run, call, value, actionId) {
|
|
1005
|
+
// 用户补充是任务输入,必须完整参与当前推理与跨轮恢复;大查询结果才使用预览。
|
|
1006
|
+
const preview = call.name === 'request_input' ? value : contextPreview(value);
|
|
1007
|
+
const observation = preview === value
|
|
1008
|
+
? value
|
|
1009
|
+
: { truncated: true, source: value?.source, ref: { runId: run.id, actionId }, preview };
|
|
1010
|
+
await this.change(run, async (tx, current) => {
|
|
1011
|
+
const pending = current.pending.slice(1), history = [
|
|
1012
|
+
...current.history,
|
|
1013
|
+
{
|
|
1014
|
+
role: 'tool',
|
|
1015
|
+
toolCallId: call.id,
|
|
1016
|
+
content: json(observation),
|
|
1017
|
+
runId: run.id,
|
|
1018
|
+
...(actionId ? { receipt: { runId: run.id, actionId } } : {}),
|
|
1019
|
+
},
|
|
1020
|
+
];
|
|
1021
|
+
await tx
|
|
1022
|
+
.updateTable('sys.ai_run')
|
|
1023
|
+
.set({
|
|
1024
|
+
history: json(history),
|
|
1025
|
+
pending: json(pending),
|
|
1026
|
+
phase: pending.length ? 'actions' : 'model',
|
|
1027
|
+
tool_calls: current.tool_calls + 1,
|
|
1028
|
+
})
|
|
1029
|
+
.where('id', '=', run.id)
|
|
1030
|
+
.execute();
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
/**
|
|
1034
|
+
* 持有一轮领取租约运行状态机,每 5 秒单次续租,组合外部停止、进程关闭、期限和失去租约信号。
|
|
1035
|
+
* 每步重新读取持久状态、处理遗留模型预留并核验身份/审批有效性;最近三次相同输入摘要触发无进展失败。
|
|
1036
|
+
* 失去租约退出;主动停机尝试重新排队,其他失败进入 fail;finally 清除续租定时器并等待在途续租结束。
|
|
1037
|
+
*/
|
|
1038
|
+
async execute(initial, outer) {
|
|
1039
|
+
const lease = new AbortController(), timeout = AbortSignal.timeout(Math.max(1, new Date(initial.deadline).getTime() - Date.now()));
|
|
1040
|
+
const signal = AbortSignal.any([lease.signal, timeout, this.stopping.signal, ...(outer ? [outer] : [])]);
|
|
1041
|
+
let renewal = null;
|
|
1042
|
+
const renew = setInterval(() => {
|
|
1043
|
+
if (renewal)
|
|
1044
|
+
return;
|
|
1045
|
+
renewal = this.kernel.app
|
|
1046
|
+
.updateTable('sys.ai_run')
|
|
1047
|
+
.set({ lease_until: new Date(Date.now() + LEASE_MS) })
|
|
1048
|
+
.where('id', '=', initial.id)
|
|
1049
|
+
.where('lease_token', '=', initial.lease_token)
|
|
1050
|
+
.where('status', '=', 'running')
|
|
1051
|
+
.where('lease_until', '>', new Date())
|
|
1052
|
+
.executeTakeFirst()
|
|
1053
|
+
.then((r) => {
|
|
1054
|
+
if (Number(r.numUpdatedRows) !== 1)
|
|
1055
|
+
lease.abort(new LostLease());
|
|
1056
|
+
}, () => lease.abort(new LostLease()))
|
|
1057
|
+
.finally(() => {
|
|
1058
|
+
renewal = null;
|
|
1059
|
+
});
|
|
1060
|
+
}, 5000);
|
|
1061
|
+
try {
|
|
1062
|
+
for (;;) {
|
|
1063
|
+
signal.throwIfAborted();
|
|
1064
|
+
const run = await this.readRun(initial.id);
|
|
1065
|
+
if (run.status !== 'running' || run.lease_token !== initial.lease_token)
|
|
1066
|
+
return;
|
|
1067
|
+
if (run.model_reservation) {
|
|
1068
|
+
await this.settleUncertain(run, 'worker_recovered');
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
const ctx = await this.identity(run);
|
|
1072
|
+
if (run.mode === 'approval')
|
|
1073
|
+
await this.approvalActive(run);
|
|
1074
|
+
const execution = { kernel: this.kernel, ontology: this.ontology, ctx, run, signal };
|
|
1075
|
+
if (run.phase === 'context')
|
|
1076
|
+
await this.prepareContext(run, ctx);
|
|
1077
|
+
else if (run.phase === 'model') {
|
|
1078
|
+
const actions = await this.kernel.app
|
|
1079
|
+
.selectFrom('sys.ai_action')
|
|
1080
|
+
.select('input_hash')
|
|
1081
|
+
.where('run_id', '=', run.id)
|
|
1082
|
+
.orderBy('created_at', 'desc')
|
|
1083
|
+
.limit(3)
|
|
1084
|
+
.execute();
|
|
1085
|
+
if (actions.length === 3 && actions.every((a) => a.input_hash === actions[0].input_hash))
|
|
1086
|
+
throw new Error('AI_NO_PROGRESS');
|
|
1087
|
+
await this.model(execution);
|
|
1088
|
+
}
|
|
1089
|
+
else if (run.phase === 'actions' && run.pending.length)
|
|
1090
|
+
await this.action(execution, run.pending[0]);
|
|
1091
|
+
else
|
|
1092
|
+
throw new Error('AI_STATE_INVALID');
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
catch (error) {
|
|
1096
|
+
if (error instanceof LostLease || lease.signal.aborted)
|
|
1097
|
+
return;
|
|
1098
|
+
await this.settleUncertain(initial, 'execution_stopped');
|
|
1099
|
+
if (this.stopping.signal.aborted || outer?.aborted) {
|
|
1100
|
+
await this.change(initial, async (tx) => {
|
|
1101
|
+
await tx
|
|
1102
|
+
.updateTable('sys.ai_run')
|
|
1103
|
+
.set({ status: 'queued', lease_token: null, lease_until: null })
|
|
1104
|
+
.where('id', '=', initial.id)
|
|
1105
|
+
.execute();
|
|
1106
|
+
}).catch(() => { });
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
await this.fail(initial, error, timeout.aborted);
|
|
1110
|
+
}
|
|
1111
|
+
finally {
|
|
1112
|
+
clearInterval(renew);
|
|
1113
|
+
await renewal;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* 将执行异常收敛为稳定代码,并在租约事务内终结运行。
|
|
1118
|
+
* 审批按 onFailure 决定系统转人工或标记失败,不能伪造模型通过/退回;人工接管不可用时仍标记失败,避免旧任务反复领取。
|
|
1119
|
+
* 租约已转移时不覆盖新执行者;原始上游错误正文不会成为公开运行错误。
|
|
1120
|
+
*/
|
|
1121
|
+
async fail(run, error, expired) {
|
|
1122
|
+
const code = isCageError(error)
|
|
1123
|
+
? error.code
|
|
1124
|
+
: expired
|
|
1125
|
+
? 'AI_TIMEOUT'
|
|
1126
|
+
: /^AI_[A-Z_]+$/.test(error?.message)
|
|
1127
|
+
? error.message
|
|
1128
|
+
: 'AI_EXECUTION_FAILED';
|
|
1129
|
+
try {
|
|
1130
|
+
await this.change(run, async (tx, current) => {
|
|
1131
|
+
if (current.mode === 'approval' && current.policy.onFailure !== 'failed') {
|
|
1132
|
+
const engine = engines({ kernel: this.kernel }).find((e) => e.route.process === current.process);
|
|
1133
|
+
// 基础设施失败明确转人工,不伪造模型的通过/退回裁决。
|
|
1134
|
+
await engine.completeAi(tx, current.approval_step_id, current.id, {
|
|
1135
|
+
decision: 'manual',
|
|
1136
|
+
reason: `系统转人工:${code}`,
|
|
1137
|
+
suggestions: [],
|
|
1138
|
+
evidence: [],
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
else if (current.mode === 'approval') {
|
|
1142
|
+
await tx
|
|
1143
|
+
.updateTable('flow.approval_step')
|
|
1144
|
+
.set({ status: 'failed' })
|
|
1145
|
+
.where('id', '=', current.approval_step_id)
|
|
1146
|
+
.where('run_id', '=', current.id)
|
|
1147
|
+
.execute();
|
|
1148
|
+
}
|
|
1149
|
+
await tx
|
|
1150
|
+
.updateTable('sys.ai_run')
|
|
1151
|
+
.set({ status: expired ? 'expired' : 'failed', error: code, lease_token: null, lease_until: null })
|
|
1152
|
+
.where('id', '=', run.id)
|
|
1153
|
+
.execute();
|
|
1154
|
+
await event(tx, run.id, 'run.status', { status: expired ? 'expired' : 'failed', error: code });
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
catch (failure) {
|
|
1158
|
+
if (failure instanceof LostLease)
|
|
1159
|
+
return;
|
|
1160
|
+
// 撤回/重提或人工接管失败也必须停下;不让不可执行的旧任务循环领取。
|
|
1161
|
+
await this.change(run, async (tx) => {
|
|
1162
|
+
await tx
|
|
1163
|
+
.updateTable('sys.ai_run')
|
|
1164
|
+
.set({
|
|
1165
|
+
status: 'failed',
|
|
1166
|
+
error: `${code}:HANDOFF_UNAVAILABLE`,
|
|
1167
|
+
lease_token: null,
|
|
1168
|
+
lease_until: null,
|
|
1169
|
+
})
|
|
1170
|
+
.where('id', '=', run.id)
|
|
1171
|
+
.execute();
|
|
1172
|
+
if (run.mode === 'approval')
|
|
1173
|
+
await tx
|
|
1174
|
+
.updateTable('flow.approval_step')
|
|
1175
|
+
.set({ status: 'failed' })
|
|
1176
|
+
.where('id', '=', run.approval_step_id)
|
|
1177
|
+
.where('status', '=', 'running')
|
|
1178
|
+
.where('run_id', '=', run.id)
|
|
1179
|
+
.execute();
|
|
1180
|
+
await event(tx, run.id, 'run.status', { status: 'failed', error: `${code}:HANDOFF_UNAVAILABLE` });
|
|
1181
|
+
}).catch((e) => {
|
|
1182
|
+
if (!(e instanceof LostLease))
|
|
1183
|
+
throw e;
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
//# sourceMappingURL=runtime.js.map
|