@springbrand/agent-runtime 0.1.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/package.json +28 -0
- package/src/db/approval.repo.ts +291 -0
- package/src/db/ext-context.repo.ts +34 -0
- package/src/db/index.ts +83 -0
- package/src/db/message-ui.repo.ts +39 -0
- package/src/db/milestone.repo.ts +96 -0
- package/src/db/runtime-event-outbox.repo.ts +89 -0
- package/src/db/schema.ts +164 -0
- package/src/db/settlement.repo.ts +104 -0
- package/src/db/steer.repo.ts +73 -0
- package/src/db/submission.repo.ts +323 -0
- package/src/index.ts +133 -0
- package/src/kernel/approval-lifecycle.ts +552 -0
- package/src/kernel/bindings.ts +898 -0
- package/src/kernel/degradation.ts +15 -0
- package/src/kernel/extensions.ts +108 -0
- package/src/kernel/profile.ts +116 -0
- package/src/kernel/public-contracts.ts +17 -0
- package/src/kernel/receipts.ts +124 -0
- package/src/kernel/recoverable-chat-agent.ts +899 -0
- package/src/kernel/state.ts +76 -0
- package/src/kernel/submission-lifecycle.ts +600 -0
- package/src/layers/context/budget/gate.ts +88 -0
- package/src/layers/orchestration/subagents/agent-types/contract.ts +78 -0
- package/src/layers/orchestration/subagents/agent-types/extract/index.ts +47 -0
- package/src/layers/orchestration/subagents/agent-types/fanout/index.ts +53 -0
- package/src/layers/orchestration/subagents/agent-types/registry.ts +16 -0
- package/src/layers/orchestration/temporary-agent/core.ts +152 -0
- package/src/layers/orchestration/temporary-agent/runner.ts +133 -0
- package/src/layers/orchestration/temporary-agent/workspace.ts +154 -0
- package/src/lib/artifacts.ts +54 -0
- package/src/lib/egress.ts +44 -0
- package/src/lib/execution-level.ts +27 -0
- package/src/lib/extension-name.ts +18 -0
- package/src/lib/host-actions.ts +57 -0
- package/src/lib/mcp.ts +86 -0
- package/src/lib/model-catalog.ts +7 -0
- package/src/lib/prompt.ts +139 -0
- package/src/lib/telemetry-dev.ts +44 -0
- package/src/pi/assembly/context.ts +510 -0
- package/src/pi/assembly/extensions.ts +661 -0
- package/src/pi/assembly/index.ts +19 -0
- package/src/pi/assembly/snapshot.ts +200 -0
- package/src/pi/message/contract.ts +8 -0
- package/src/pi/message/conversion.ts +73 -0
- package/src/pi/message/index.ts +3 -0
- package/src/pi/message/projection.ts +604 -0
- package/src/pi/runtime-adapter/assembly.ts +552 -0
- package/src/pi/runtime-adapter/execution.ts +683 -0
- package/src/pi/runtime-adapter/index.ts +232 -0
- package/src/pi/runtime-adapter/models.ts +243 -0
- package/src/pi/runtime-adapter/recovery.ts +805 -0
- package/src/pi/runtime-adapter/transcript.ts +825 -0
- package/src/pi/session/index.ts +24 -0
- package/src/pi/session/storage.ts +353 -0
- package/src/pi/tool/ai-adapter.ts +100 -0
- package/src/pi/tool/base.ts +110 -0
- package/src/pi/tool/compiler.ts +444 -0
- package/src/pi/tool/core-host.ts +48 -0
- package/src/pi/tool/core.ts +251 -0
- package/src/pi/tool/index.ts +32 -0
- package/src/pi/tool/mcp.ts +319 -0
- package/src/pi/tool/schedule.ts +198 -0
- package/src/pi/tool/skill.ts +455 -0
- package/src/pi/tool/subagent.ts +148 -0
- package/src/pi/tool/web-search/api.ts +1292 -0
- package/src/pi/tool/web-search/index.ts +2 -0
- package/src/pi/tool/web-search/web-search.ts +127 -0
- package/src/pi/tool/workspace-sandbox.ts +664 -0
- package/src/pi/turn/approval.ts +181 -0
- package/src/pi/turn/index.ts +62 -0
- package/src/pi/turn/tool-recovery.ts +792 -0
- package/src/plugins.ts +1024 -0
- package/src/runtime-agent.ts +654 -0
- package/src/runtime.ts +2880 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ApprovalReceipt } from "./receipts";
|
|
2
|
+
|
|
3
|
+
export type RuntimeLoadPhase = "config" | "plugins" | "mcp" | "pi";
|
|
4
|
+
|
|
5
|
+
export type RuntimeActivity = "idle" | "working" | "needs-input";
|
|
6
|
+
|
|
7
|
+
export interface RuntimeActivityProjection {
|
|
8
|
+
activity: RuntimeActivity;
|
|
9
|
+
revision: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Runtime 最近一次装载尝试的公开状态。
|
|
14
|
+
*
|
|
15
|
+
* `available` 与本次尝试分开表达:强制重载失败时,旧 Runtime 仍可能继续服务。
|
|
16
|
+
* 该状态由 Agents SDK 的 Agent state 同步给浏览器,不包含底层异常文本。
|
|
17
|
+
*/
|
|
18
|
+
export type RuntimeLoadState =
|
|
19
|
+
| {
|
|
20
|
+
status: "idle";
|
|
21
|
+
available: false;
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
status: "loading";
|
|
25
|
+
phase: RuntimeLoadPhase;
|
|
26
|
+
available: boolean;
|
|
27
|
+
startedAt: number;
|
|
28
|
+
updatedAt: number;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
status: "ready";
|
|
32
|
+
available: true;
|
|
33
|
+
startedAt: number;
|
|
34
|
+
completedAt: number;
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
status: "error";
|
|
38
|
+
phase: RuntimeLoadPhase;
|
|
39
|
+
available: boolean;
|
|
40
|
+
startedAt: number;
|
|
41
|
+
failedAt: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface RuntimeQueuedSubmission {
|
|
45
|
+
submissionId: string;
|
|
46
|
+
messageId: string;
|
|
47
|
+
preview: string;
|
|
48
|
+
position: number;
|
|
49
|
+
createdAt: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RuntimeTurnState {
|
|
53
|
+
activeSubmissionId?: string;
|
|
54
|
+
steerable: boolean;
|
|
55
|
+
hasPendingSteer: boolean;
|
|
56
|
+
queued: RuntimeQueuedSubmission[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 保存 Kernel 在当前会话里自己维护的审批状态。
|
|
61
|
+
*
|
|
62
|
+
* @remarks
|
|
63
|
+
* `RuntimeAgent` 把它作为 Agent state 的形状,并在审批记录变化时读写。
|
|
64
|
+
*
|
|
65
|
+
* 它故意不保存用户、Agent 或 Session 等业务身份,避免 Runtime 越过 Host 的归属边界。
|
|
66
|
+
*
|
|
67
|
+
* `Runtime`、`Kernel` 与 `Host` 等核心术语见 `../index.ts`。
|
|
68
|
+
*/
|
|
69
|
+
export interface RuntimeState {
|
|
70
|
+
/** 当前实例的 Runtime 装载状态;旧状态记录没有该字段时等同 `idle`。 */
|
|
71
|
+
runtimeLoad?: RuntimeLoadState;
|
|
72
|
+
/** 供 Host 列表持久化的粗粒度活动状态;Submission 与 Approval 仍是执行真相。 */
|
|
73
|
+
activity?: RuntimeActivityProjection;
|
|
74
|
+
approvals?: ApprovalReceipt[];
|
|
75
|
+
turn?: RuntimeTurnState;
|
|
76
|
+
}
|
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
import { TurnQueue } from "agents/chat";
|
|
2
|
+
import {
|
|
3
|
+
isTerminalSubmissionStatus,
|
|
4
|
+
type SubmissionStatus,
|
|
5
|
+
} from "../db/submission.repo";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 本文件统一管理一次提交从接收到结束的过程。
|
|
9
|
+
*
|
|
10
|
+
* @remarks
|
|
11
|
+
* Runtime、Port 等核心术语沿用包入口 `../index.ts` 的定义,这里不重复解释。
|
|
12
|
+
*
|
|
13
|
+
* `AgentRuntimeKernel` 通过本文件把去重、串行执行、恢复和取消收口到同一条路径。
|
|
14
|
+
*
|
|
15
|
+
* 这些规则集中在一起,是为了避免普通提交、定时提交和恢复各自维护一套状态判断。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// #region 公开契约
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 表示生命周期管理所需的一条最小提交记录。
|
|
22
|
+
*
|
|
23
|
+
* @remarks
|
|
24
|
+
* 存储实现返回该形状,`SubmissionLifecycle` 在接收、等待和取消时读取它。
|
|
25
|
+
*
|
|
26
|
+
* 这里只要求生命周期判断必需的字段,业务侧可以通过泛型保留更多持久化字段。
|
|
27
|
+
*/
|
|
28
|
+
export interface SubmissionRecord {
|
|
29
|
+
submissionId: string;
|
|
30
|
+
requestId: string;
|
|
31
|
+
idempotencyKey?: string | null;
|
|
32
|
+
status: SubmissionStatus;
|
|
33
|
+
accepted: boolean;
|
|
34
|
+
abortReason?: string | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 提供生命周期判断和写入所需的持久化操作。
|
|
39
|
+
*
|
|
40
|
+
* @remarks
|
|
41
|
+
* `AgentRuntimeKernel` 在构造 `SubmissionLifecycle` 时传入实现,生命周期方法随后按需调用。
|
|
42
|
+
*
|
|
43
|
+
* 接口保持同步,是因为接收提交时必须在一个同步事务里完成查重、容量检查和创建。
|
|
44
|
+
*/
|
|
45
|
+
export interface SubmissionStore<TSubmission extends SubmissionRecord> {
|
|
46
|
+
/**
|
|
47
|
+
* 在一个同步事务中运行接收或取消写入。
|
|
48
|
+
*
|
|
49
|
+
* @remarks
|
|
50
|
+
* `admit` 和 `cancel` 在需要同时检查并修改持久化状态时调用,回调内只能使用同步存储操作。
|
|
51
|
+
*
|
|
52
|
+
* 不要把异步准备移入回调;当前 SQLite-backed Durable Object 的 `transactionSync` 要求回调同步完成。
|
|
53
|
+
*/
|
|
54
|
+
transaction<T>(run: () => T): T;
|
|
55
|
+
/**
|
|
56
|
+
* 按提交标识读取最新记录。
|
|
57
|
+
*
|
|
58
|
+
* @remarks
|
|
59
|
+
* 查询、等待、恢复和结束路径都会调用,找不到时返回 `null`。
|
|
60
|
+
*
|
|
61
|
+
* 每次读取最新值可以避免用旧快照覆盖已经写入的终态或取消意图。
|
|
62
|
+
*/
|
|
63
|
+
find(submissionId: string): TSubmission | null;
|
|
64
|
+
/**
|
|
65
|
+
* 按请求标识查找已有提交。
|
|
66
|
+
*
|
|
67
|
+
* @remarks
|
|
68
|
+
* 接收和按请求停止时调用,同一请求已存在时应返回那条记录。
|
|
69
|
+
*
|
|
70
|
+
* 请求级查重是没有幂等键时的固定后备规则。
|
|
71
|
+
*/
|
|
72
|
+
findByRequestId(requestId: string): TSubmission | null;
|
|
73
|
+
/**
|
|
74
|
+
* 按调用方提供的幂等键查找已有提交。
|
|
75
|
+
*
|
|
76
|
+
* @remarks
|
|
77
|
+
* 接收路径只在输入带幂等键时调用,命中后复用已有提交。
|
|
78
|
+
*
|
|
79
|
+
* 幂等键优先于请求标识,不能调换,否则同一业务动作可能被当成两次提交。
|
|
80
|
+
*/
|
|
81
|
+
findByIdempotencyKey(key: string): TSubmission | null;
|
|
82
|
+
/**
|
|
83
|
+
* 统计仍在等待或运行的提交。
|
|
84
|
+
*
|
|
85
|
+
* @remarks
|
|
86
|
+
* 接收、稳定等待和 Runtime 忙碌检查会调用。
|
|
87
|
+
*
|
|
88
|
+
* 该计数来自持久层而不是内存 Map,因此重启后的未完成工作仍会阻止新提交。
|
|
89
|
+
*/
|
|
90
|
+
countUnfinished(): number;
|
|
91
|
+
countPending(): number;
|
|
92
|
+
findRunning(): TSubmission | null;
|
|
93
|
+
findNextPending(): TSubmission | null;
|
|
94
|
+
/**
|
|
95
|
+
* 列出全部等待或运行中的提交标识。
|
|
96
|
+
*
|
|
97
|
+
* @remarks
|
|
98
|
+
* `stop` 没有指定请求时调用,并逐条走统一取消路径。
|
|
99
|
+
*
|
|
100
|
+
* 只返回标识可以避免停止入口复制完整记录或状态判断。
|
|
101
|
+
*/
|
|
102
|
+
/**
|
|
103
|
+
* 持久化一条取消原因。
|
|
104
|
+
*
|
|
105
|
+
* @remarks
|
|
106
|
+
* `cancel` 在中断执行器之前调用,执行器稍后激活时也能读到该原因。
|
|
107
|
+
*
|
|
108
|
+
* 先持久化意图再触发内存中断,可以让取消跨越执行启动窗口和实例重启。
|
|
109
|
+
*/
|
|
110
|
+
updateAbortReason(submissionId: string, reason: string): void;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const MAX_PENDING_SUBMISSIONS = 20;
|
|
114
|
+
|
|
115
|
+
export class SubmissionQueueFullError extends Error {
|
|
116
|
+
readonly code = "queue_full";
|
|
117
|
+
|
|
118
|
+
constructor() {
|
|
119
|
+
super(`Pi submission queue is full (${MAX_PENDING_SUBMISSIONS})`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* 表示执行器交给终态提交器的结果类别。
|
|
125
|
+
*
|
|
126
|
+
* @remarks
|
|
127
|
+
* 执行和恢复路径在调用 `finish` 时提供该值。
|
|
128
|
+
*
|
|
129
|
+
* 生命周期只区分成功、失败和取消,具体持久化状态由调用方的 `commitTerminal` 统一映射。
|
|
130
|
+
*/
|
|
131
|
+
export type SubmissionOutcome = "succeeded" | "failed" | "aborted";
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* 描述接收一次提交所需的标识和延迟创建步骤。
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* 普通消息和定时消息在调用 `submit` 或 `submitWhenStable` 前创建该对象。
|
|
138
|
+
*
|
|
139
|
+
* `prepareAdmission` 先做异步准备,再返回同步创建函数,使真正写入可以留在事务边界内。
|
|
140
|
+
*/
|
|
141
|
+
export interface SubmissionInput<
|
|
142
|
+
TSubmission extends SubmissionRecord,
|
|
143
|
+
> {
|
|
144
|
+
requestId: string;
|
|
145
|
+
idempotencyKey?: string;
|
|
146
|
+
/**
|
|
147
|
+
* 准备接收所需数据,并返回同步创建记录的函数。
|
|
148
|
+
*
|
|
149
|
+
* @remarks
|
|
150
|
+
* `submit` 只在快速查重未命中后调用;返回的函数由 `admit` 放进同步事务执行。
|
|
151
|
+
*
|
|
152
|
+
* 两阶段形状隔开异步装配和原子写入,不能直接改成在事务里返回 Promise。
|
|
153
|
+
*/
|
|
154
|
+
prepareAdmission(): Promise<() => TSubmission>;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 同时返回接收回执和最终结果。
|
|
159
|
+
*
|
|
160
|
+
* @remarks
|
|
161
|
+
* 交互入口通常立即读取 `receipt` 并把 `completion` 交给 `waitUntil`,定时入口则等待完成。
|
|
162
|
+
*
|
|
163
|
+
* 分开两个阶段可以尽快确认是否接收,同时不丢失最终持久化结果。
|
|
164
|
+
*/
|
|
165
|
+
export interface SubmissionHandle<
|
|
166
|
+
TSubmission extends SubmissionRecord,
|
|
167
|
+
> {
|
|
168
|
+
receipt: TSubmission;
|
|
169
|
+
completion: Promise<TSubmission>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// #endregion
|
|
173
|
+
|
|
174
|
+
// #region 内部依赖
|
|
175
|
+
|
|
176
|
+
interface SubmissionLifecycleOptions<
|
|
177
|
+
TSubmission extends SubmissionRecord,
|
|
178
|
+
TActive,
|
|
179
|
+
> {
|
|
180
|
+
store: SubmissionStore<TSubmission>;
|
|
181
|
+
// 作用:清理上一次聊天留下的终态标记。
|
|
182
|
+
// 调用:`admit` 只在新提交成功写入后调用。
|
|
183
|
+
// 原因:重复提交只加入旧工作,不应清除现有终态。
|
|
184
|
+
clearTerminal(): Promise<void>;
|
|
185
|
+
// 作用:执行或恢复一条已持久化的提交。
|
|
186
|
+
// 调用:`start` 在 `TurnQueue` 轮到该提交时调用。
|
|
187
|
+
// 原因:生命周期只管调度,具体 Pi Turn 执行仍由 Runtime 负责。
|
|
188
|
+
execute(submissionId: string, recovery: boolean): Promise<TSubmission>;
|
|
189
|
+
// 作用:记录可恢复的取消意图。
|
|
190
|
+
// 调用:`cancel` 在同一存储事务内与取消原因一起写入。
|
|
191
|
+
// 原因:持久化里程碑可以让恢复路径看到已经发生的取消。
|
|
192
|
+
appendAbortIntent(submission: TSubmission, reason: string): void;
|
|
193
|
+
// 作用:把一条非终态提交写成最终结果。
|
|
194
|
+
// 调用:`finish` 在重读并确认仍非终态后调用。
|
|
195
|
+
// 原因:状态映射和其他终态副作用必须由 Runtime 在一个边界完成。
|
|
196
|
+
commitTerminal(
|
|
197
|
+
submission: TSubmission,
|
|
198
|
+
outcome: SubmissionOutcome,
|
|
199
|
+
message?: string,
|
|
200
|
+
): Promise<TSubmission>;
|
|
201
|
+
// 作用:通知当前执行器中断运行。
|
|
202
|
+
// 调用:`cancel` 命中活动执行器,或 `activate` 发现早到取消时调用。
|
|
203
|
+
// 原因:生命周期不依赖具体执行器类型,只通过这个回调中断。
|
|
204
|
+
abortActive(active: TActive): void;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// #endregion
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 让所有提交共用同一套接收、执行、恢复和取消规则。
|
|
211
|
+
*
|
|
212
|
+
* @remarks
|
|
213
|
+
* `AgentRuntimeKernel` 在构造时创建一个实例,之后普通消息、定时消息和恢复入口都复用它。
|
|
214
|
+
*
|
|
215
|
+
* 持久化状态负责跨重启事实,内存 Map 只合并当前实例内的重复执行,二者不能互相替代。
|
|
216
|
+
*/
|
|
217
|
+
export class SubmissionLifecycle<
|
|
218
|
+
TSubmission extends SubmissionRecord,
|
|
219
|
+
TActive = never,
|
|
220
|
+
> {
|
|
221
|
+
private readonly queue = new TurnQueue();
|
|
222
|
+
private readonly executions = new Map<string, Promise<TSubmission>>();
|
|
223
|
+
private readonly activeBySubmission = new Map<string, TActive>();
|
|
224
|
+
|
|
225
|
+
// #region 提交与恢复
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 用 Runtime 提供的存储和执行回调创建生命周期管理器。
|
|
229
|
+
*
|
|
230
|
+
* @remarks
|
|
231
|
+
* `AgentRuntimeKernel` 的构造函数在数据库初始化后调用一次。
|
|
232
|
+
*
|
|
233
|
+
* 依赖通过一个选项对象传入,使状态规则留在这里,业务执行和持久化细节留在 Runtime。
|
|
234
|
+
*/
|
|
235
|
+
constructor(
|
|
236
|
+
private readonly options: SubmissionLifecycleOptions<
|
|
237
|
+
TSubmission,
|
|
238
|
+
TActive
|
|
239
|
+
>,
|
|
240
|
+
) {}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* 接收新提交,或让重复请求加入已有提交。
|
|
244
|
+
*
|
|
245
|
+
* @remarks
|
|
246
|
+
* 普通消息入口调用它;调用方读取 `receipt`,并等待或托管 `completion`。
|
|
247
|
+
*
|
|
248
|
+
* 先快速查重可跳过昂贵准备,事务内再次查重则覆盖异步准备期间出现的竞争。
|
|
249
|
+
*/
|
|
250
|
+
async submit(
|
|
251
|
+
input: SubmissionInput<TSubmission>,
|
|
252
|
+
): Promise<SubmissionHandle<TSubmission>> {
|
|
253
|
+
const duplicate = this.findDuplicate(input);
|
|
254
|
+
if (duplicate) return this.join(duplicate);
|
|
255
|
+
const admitted = await this.admit(
|
|
256
|
+
input,
|
|
257
|
+
await input.prepareAdmission(),
|
|
258
|
+
);
|
|
259
|
+
if (admitted.admitted) this.pump();
|
|
260
|
+
return {
|
|
261
|
+
receipt: admitted.submission,
|
|
262
|
+
completion: this.waitForTerminal(
|
|
263
|
+
admitted.submission.submissionId,
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 等 Runtime 暂时没有活动提交后再接收新提交。
|
|
270
|
+
*
|
|
271
|
+
* @remarks
|
|
272
|
+
* 定时任务入口调用它;超时返回 `null`,重复请求则直接加入已有提交。
|
|
273
|
+
*
|
|
274
|
+
* 等待发生在异步准备之前,避免稳定性超时时做无用装配;最后仍调用 `submit` 重新查重和接收。
|
|
275
|
+
*/
|
|
276
|
+
async submitWhenStable(
|
|
277
|
+
input: SubmissionInput<TSubmission>,
|
|
278
|
+
timeoutMs: number,
|
|
279
|
+
): Promise<SubmissionHandle<TSubmission> | null> {
|
|
280
|
+
const duplicate = this.findDuplicate(input);
|
|
281
|
+
if (duplicate) return this.join(duplicate);
|
|
282
|
+
if (!await this.waitUntilStable(timeoutMs)) return null;
|
|
283
|
+
return this.submit(input);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 从持久化记录重新启动一条未完成提交。
|
|
288
|
+
*
|
|
289
|
+
* @remarks
|
|
290
|
+
* Agent 启动恢复、审批继续和恢复 Port 在已经知道提交标识时调用。
|
|
291
|
+
*
|
|
292
|
+
* 它复用 `start` 的实例内去重和串行队列,避免重复唤醒产生两个执行器。
|
|
293
|
+
*/
|
|
294
|
+
recover(submissionId: string): Promise<TSubmission> {
|
|
295
|
+
return this.start(submissionId, true);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
recoverHead(): Promise<TSubmission | null> {
|
|
299
|
+
const running = this.options.store.findRunning();
|
|
300
|
+
if (running) return this.start(running.submissionId, true);
|
|
301
|
+
const pending = this.options.store.findNextPending();
|
|
302
|
+
return pending
|
|
303
|
+
? this.start(pending.submissionId)
|
|
304
|
+
: Promise.resolve(null);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 作用:在一个同步事务内最终决定接收新提交还是复用旧提交。
|
|
308
|
+
// 调用:`submit` 完成异步准备后调用,并把同步创建函数传进来。
|
|
309
|
+
// 原因:事务内重查幂等键、请求标识和活动数,才能关住准备期间的状态变化。
|
|
310
|
+
private async admit(
|
|
311
|
+
input: SubmissionInput<TSubmission>,
|
|
312
|
+
create: () => TSubmission,
|
|
313
|
+
): Promise<{ submission: TSubmission; admitted: boolean }> {
|
|
314
|
+
const result = this.options.store.transaction(() => {
|
|
315
|
+
if (input.idempotencyKey) {
|
|
316
|
+
const duplicate = this.options.store.findByIdempotencyKey(
|
|
317
|
+
input.idempotencyKey,
|
|
318
|
+
);
|
|
319
|
+
if (duplicate) {
|
|
320
|
+
return {
|
|
321
|
+
submission: { ...duplicate, accepted: false } as TSubmission,
|
|
322
|
+
admitted: false,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const requestDuplicate = this.options.store.findByRequestId(
|
|
327
|
+
input.requestId,
|
|
328
|
+
);
|
|
329
|
+
if (requestDuplicate) {
|
|
330
|
+
return {
|
|
331
|
+
submission: {
|
|
332
|
+
...requestDuplicate,
|
|
333
|
+
accepted: false,
|
|
334
|
+
} as TSubmission,
|
|
335
|
+
admitted: false,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
this.options.store.countPending() >=
|
|
340
|
+
MAX_PENDING_SUBMISSIONS
|
|
341
|
+
) {
|
|
342
|
+
throw new SubmissionQueueFullError();
|
|
343
|
+
}
|
|
344
|
+
return { submission: create(), admitted: true };
|
|
345
|
+
});
|
|
346
|
+
if (result.admitted) await this.options.clearTerminal();
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 作用:按幂等键优先、请求标识其次的规则查找重复提交。
|
|
351
|
+
// 调用:`submit` 和 `submitWhenStable` 在异步准备或等待前调用。
|
|
352
|
+
// 原因:共享查找顺序可避免普通与定时提交对“重复”作出不同判断。
|
|
353
|
+
private findDuplicate(
|
|
354
|
+
input: Pick<
|
|
355
|
+
SubmissionInput<TSubmission>,
|
|
356
|
+
"requestId" | "idempotencyKey"
|
|
357
|
+
>,
|
|
358
|
+
): TSubmission | null {
|
|
359
|
+
return (
|
|
360
|
+
(input.idempotencyKey
|
|
361
|
+
? this.options.store.findByIdempotencyKey(input.idempotencyKey)
|
|
362
|
+
: null) ??
|
|
363
|
+
this.options.store.findByRequestId(input.requestId)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 作用:为重复请求生成未接收回执,并连到原提交的完成结果。
|
|
368
|
+
// 调用:两个提交入口在事务外查重命中时调用。
|
|
369
|
+
// 原因:复用同一条持久化工作,同时用 `accepted: false` 告诉调用方没有新建执行。
|
|
370
|
+
private join(
|
|
371
|
+
submission: TSubmission,
|
|
372
|
+
): SubmissionHandle<TSubmission> {
|
|
373
|
+
return {
|
|
374
|
+
receipt: { ...submission, accepted: false } as TSubmission,
|
|
375
|
+
completion: this.waitForTerminal(submission.submissionId),
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 作用:启动一次串行执行,并合并当前实例内对同一提交的重复唤醒。
|
|
380
|
+
// 调用:新提交和恢复入口在已有持久化记录后调用。
|
|
381
|
+
// 原因:`TurnQueue` 防止不同 Pi Turn 重叠,`executions` 则防止同一标识重复入队。
|
|
382
|
+
private start(
|
|
383
|
+
submissionId: string,
|
|
384
|
+
recovery = false,
|
|
385
|
+
): Promise<TSubmission> {
|
|
386
|
+
const existing = this.executions.get(submissionId);
|
|
387
|
+
if (existing) return existing;
|
|
388
|
+
let shouldPump = false;
|
|
389
|
+
const started = this.queue
|
|
390
|
+
.enqueue(submissionId, () =>
|
|
391
|
+
this.options.execute(submissionId, recovery),
|
|
392
|
+
)
|
|
393
|
+
.then((outcome) => {
|
|
394
|
+
// TODO(待确认): 当前类没有调用 `queue.reset()`,按现有调用链不会产生 `stale` 结果。
|
|
395
|
+
if (outcome.status !== "stale") {
|
|
396
|
+
shouldPump = isTerminalSubmissionStatus(
|
|
397
|
+
outcome.value.status,
|
|
398
|
+
);
|
|
399
|
+
return outcome.value;
|
|
400
|
+
}
|
|
401
|
+
const submission = this.options.store.find(submissionId);
|
|
402
|
+
if (!submission) {
|
|
403
|
+
throw new Error(`Unknown Pi submission: ${submissionId}`);
|
|
404
|
+
}
|
|
405
|
+
shouldPump = isTerminalSubmissionStatus(submission.status);
|
|
406
|
+
return submission;
|
|
407
|
+
})
|
|
408
|
+
.finally(() => {
|
|
409
|
+
if (this.executions.get(submissionId) === started) {
|
|
410
|
+
this.executions.delete(submissionId);
|
|
411
|
+
}
|
|
412
|
+
if (shouldPump) this.pump();
|
|
413
|
+
});
|
|
414
|
+
this.executions.set(submissionId, started);
|
|
415
|
+
return started;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
private pump(): void {
|
|
419
|
+
if (this.options.store.findRunning()) return;
|
|
420
|
+
const next = this.options.store.findNextPending();
|
|
421
|
+
if (!next) return;
|
|
422
|
+
void this.start(next.submissionId).catch(() => undefined);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 作用:等待指定提交的内存执行或持久化终态。
|
|
426
|
+
// 调用:`join` 和事务内才发现的重复提交使用它返回共享的 `completion`。
|
|
427
|
+
// 原因:内存 Promise 只覆盖本实例,固定间隔查持久层才能看到其他唤醒写入的终态。
|
|
428
|
+
private async waitForTerminal(
|
|
429
|
+
submissionId: string,
|
|
430
|
+
): Promise<TSubmission> {
|
|
431
|
+
// TODO(待确认): 持久层若长期保持非终态且没有执行者,这里没有超时或外部唤醒上限。
|
|
432
|
+
for (;;) {
|
|
433
|
+
const execution = this.executions.get(submissionId);
|
|
434
|
+
if (execution) return execution;
|
|
435
|
+
const submission = this.options.store.find(submissionId);
|
|
436
|
+
if (!submission) {
|
|
437
|
+
throw new Error(`Unknown Pi submission: ${submissionId}`);
|
|
438
|
+
}
|
|
439
|
+
if (isTerminalSubmissionStatus(submission.status)) return submission;
|
|
440
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// #endregion
|
|
445
|
+
|
|
446
|
+
// #region 查询与结束
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* 读取指定提交的最新持久化记录。
|
|
450
|
+
*
|
|
451
|
+
* @remarks
|
|
452
|
+
* Runtime 的 `getSubmission` RPC 在按标识查询回执时调用。
|
|
453
|
+
*
|
|
454
|
+
* 它不返回内存执行状态,持久化记录仍是对外查询的唯一事实来源。
|
|
455
|
+
*/
|
|
456
|
+
get(submissionId: string): TSubmission | null {
|
|
457
|
+
return this.options.store.find(submissionId);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* 在提交仍未结束时写入一次最终结果。
|
|
462
|
+
*
|
|
463
|
+
* @remarks
|
|
464
|
+
* 正常执行、错误处理、恢复和取消路径都在得出结果后调用。
|
|
465
|
+
*
|
|
466
|
+
* 写入前重读最新记录并保留已有终态,避免较晚到达的结果覆盖先完成的结果。
|
|
467
|
+
*/
|
|
468
|
+
async finish(
|
|
469
|
+
submission: TSubmission,
|
|
470
|
+
outcome: SubmissionOutcome,
|
|
471
|
+
message?: string,
|
|
472
|
+
): Promise<TSubmission> {
|
|
473
|
+
const latest = this.options.store.find(submission.submissionId);
|
|
474
|
+
if (!latest) {
|
|
475
|
+
throw new Error(`Unknown Pi submission: ${submission.submissionId}`);
|
|
476
|
+
}
|
|
477
|
+
if (isTerminalSubmissionStatus(latest.status)) return latest;
|
|
478
|
+
return this.options.commitTerminal(latest, outcome, message);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* 判断持久层是否仍有等待或运行中的提交。
|
|
483
|
+
*
|
|
484
|
+
* @remarks
|
|
485
|
+
* Runtime 在重载配置、清空聊天、Transcript 检查和定时等待时调用。
|
|
486
|
+
*
|
|
487
|
+
* 读取持久层而不是只看 `executions`,才能覆盖重启后尚未恢复的提交。
|
|
488
|
+
*/
|
|
489
|
+
isBusy(): boolean {
|
|
490
|
+
return this.options.store.countUnfinished() > 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// 作用:在有限时间内轮询 Runtime 是否已无活动提交。
|
|
494
|
+
// 调用:`submitWhenStable` 在准备定时提交之前调用。
|
|
495
|
+
// 原因:每次最多等待 50ms 并截断到 deadline,可以不超过调用方给定的窗口。
|
|
496
|
+
private async waitUntilStable(timeoutMs: number): Promise<boolean> {
|
|
497
|
+
const deadline = Date.now() + timeoutMs;
|
|
498
|
+
while (this.isBusy()) {
|
|
499
|
+
const remaining = deadline - Date.now();
|
|
500
|
+
if (remaining <= 0) return false;
|
|
501
|
+
await new Promise((resolve) =>
|
|
502
|
+
setTimeout(resolve, Math.min(50, remaining)),
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// #endregion
|
|
509
|
+
|
|
510
|
+
// #region 活动执行与取消
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* 登记一条提交当前使用的执行器,并返回配对的解除函数。
|
|
514
|
+
*
|
|
515
|
+
* @remarks
|
|
516
|
+
* Runtime 在 Pi Turn 真正开始前调用,并在执行结束的 `finally` 中调用返回函数。
|
|
517
|
+
*
|
|
518
|
+
* 登记后立即重读取消原因,可以补上取消已写入、执行器尚未激活的时间窗口。
|
|
519
|
+
*/
|
|
520
|
+
activate(submission: TSubmission, active: TActive): () => void {
|
|
521
|
+
this.activeBySubmission.set(submission.submissionId, active);
|
|
522
|
+
const latest = this.options.store.find(submission.submissionId);
|
|
523
|
+
if (latest?.abortReason) this.options.abortActive(active);
|
|
524
|
+
return () => {
|
|
525
|
+
if (this.activeBySubmission.get(submission.submissionId) === active) {
|
|
526
|
+
this.activeBySubmission.delete(submission.submissionId);
|
|
527
|
+
}
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* 返回当前唯一活动的执行器。
|
|
533
|
+
*
|
|
534
|
+
* @remarks
|
|
535
|
+
* Extension Host 发送消息时调用;有活动 Turn 就 steer,没有则写入 Transcript。
|
|
536
|
+
*
|
|
537
|
+
* 它依赖接收检查和串行队列维持最多一个活动执行器,不能在允许并行 Turn 后继续只取第一个值。
|
|
538
|
+
*/
|
|
539
|
+
currentActive(): TActive | undefined {
|
|
540
|
+
return this.activeBySubmission.values().next().value;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* 为一条未结束提交记录取消,并尽快中断活动执行器。
|
|
545
|
+
*
|
|
546
|
+
* @remarks
|
|
547
|
+
* 客户端取消事件和 `cancelSubmissionById` RPC 按提交标识调用;不存在或已结束时返回 `ok: false`。
|
|
548
|
+
*
|
|
549
|
+
* 先在事务中保存取消原因和恢复意图,再按活动、已排队或未执行三种情况处理,避免丢失启动窗口内的取消。
|
|
550
|
+
*/
|
|
551
|
+
async cancel(
|
|
552
|
+
submissionId: string,
|
|
553
|
+
reason = "Cancelled",
|
|
554
|
+
): Promise<{ ok: boolean }> {
|
|
555
|
+
const submission = this.options.store.find(submissionId);
|
|
556
|
+
if (!submission || isTerminalSubmissionStatus(submission.status)) {
|
|
557
|
+
return { ok: false };
|
|
558
|
+
}
|
|
559
|
+
this.options.store.transaction(() => {
|
|
560
|
+
this.options.store.updateAbortReason(submissionId, reason);
|
|
561
|
+
this.options.appendAbortIntent(submission, reason);
|
|
562
|
+
});
|
|
563
|
+
const active = this.activeBySubmission.get(submissionId);
|
|
564
|
+
if (active) this.options.abortActive(active);
|
|
565
|
+
else {
|
|
566
|
+
await this.finish(submission, "aborted", reason);
|
|
567
|
+
this.pump();
|
|
568
|
+
}
|
|
569
|
+
return { ok: true };
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* 停止某个请求对应的提交,或停止所有未结束提交。
|
|
574
|
+
*
|
|
575
|
+
* @remarks
|
|
576
|
+
* Runtime 的 `stopTurn` RPC 调用;传入请求标识时只处理匹配项,不传时处理全部活动记录。
|
|
577
|
+
*
|
|
578
|
+
* 所有目标都逐条复用 `cancel`,确保单条取消和批量停止遵守同一套持久化顺序。
|
|
579
|
+
*/
|
|
580
|
+
async stop(
|
|
581
|
+
requestId?: string,
|
|
582
|
+
reason = "Stopped",
|
|
583
|
+
): Promise<{ ok: boolean }> {
|
|
584
|
+
const submissionIds = requestId
|
|
585
|
+
? (() => {
|
|
586
|
+
const submission = this.options.store.findByRequestId(requestId);
|
|
587
|
+
return submission ? [submission.submissionId] : [];
|
|
588
|
+
})()
|
|
589
|
+
: (() => {
|
|
590
|
+
const submission = this.options.store.findRunning();
|
|
591
|
+
return submission ? [submission.submissionId] : [];
|
|
592
|
+
})();
|
|
593
|
+
for (const submissionId of submissionIds) {
|
|
594
|
+
await this.cancel(submissionId, reason);
|
|
595
|
+
}
|
|
596
|
+
return { ok: true };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// #endregion
|
|
600
|
+
}
|