@ai-zen/agents-sdk 1.0.0-alpha.20 → 1.0.0-alpha.21
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/CHANGELOG.md +26 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/runtime/HandoffService.d.ts +46 -0
- package/dist/runtime/HandoffService.js +108 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.0-alpha.21] - 2026-10-28
|
|
4
|
+
|
|
5
|
+
### ✨ 新增
|
|
6
|
+
|
|
7
|
+
- **交接/迁移支持取消(`AbortSignal`)** —— 此前 `handoff()` / `migrate()` 一旦开始就没有任何中止手段,宿主按 Ctrl+C 也停不下来:
|
|
8
|
+
- `summary` 模式的独立模型请求根本没有任何取消点;
|
|
9
|
+
- `self` 模式能被 `agent.abort()` 打断,但中止后被误判成「模型没写出文档」,于是重试一轮、再降级 `summary` 再跑一轮——用户以为取消了,实际多烧两轮全量输入。
|
|
10
|
+
|
|
11
|
+
现在 `HandoffParams` 新增 `signal?: AbortSignal`,两个执行者都认它:
|
|
12
|
+
|
|
13
|
+
- `summary`:`signal` 作为 **RequestOptions(第二参数)** 传给 `chat.completions.create`,请求会真的被中断;
|
|
14
|
+
- `self`:每个 `agent.send()` 之后**主动检查** `aborted`(因为 `abort()` 不抛错),取消即抛 `HandoffAbortedError`,**不重试、不降级**;
|
|
15
|
+
- `runHandoff()` 在阶段之间加取消点;`migrate()` 被取消时**不改动会话历史**(不会留下「已迁移」的假象)。
|
|
16
|
+
|
|
17
|
+
- **导出 `HandoffAbortedError`** —— 取消单独成类型,调用方才能把「用户按了 Ctrl+C」与「交接真的失败」分开处理:前者静默收尾,后者才该报错。
|
|
18
|
+
|
|
19
|
+
### 🐛 修复
|
|
20
|
+
|
|
21
|
+
- **取消后清掉写了一半的交接文档** —— 只删本次新建的(同名旧文件不碰)。半成品比没有更糟:下一轮接手方会把它当成真实交接读进去。
|
|
22
|
+
|
|
23
|
+
### ✅ 测试
|
|
24
|
+
|
|
25
|
+
- 全量 **57 文件 / 608 例通过**(6 skipped)。
|
|
26
|
+
- `HandoffService.test.ts` **24 → 30 例**:signal 透传 RequestOptions、summary 取消不落盘、self 取消不重试不降级、半成品清理、migrate 取消不改历史、未取消时不误伤正常路径。
|
|
27
|
+
- 破坏性验证:删掉 `self` 的取消检查 → 「不重试、不降级」用例红。
|
|
28
|
+
|
|
3
29
|
## [1.0.0-alpha.20] - 2026-10-27
|
|
4
30
|
|
|
5
31
|
### ✨ 新增
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Desktop ──┘ │
|
|
|
21
21
|
types ← pure types, zero business dependencies (incl. ToolEnv)
|
|
22
22
|
config ← ConfigManager + constants: config.json read/write + directory setup + factory defaults
|
|
23
23
|
crud ← capability-entity CRUD (Agent definitions; conversations/drafts are persisted by each consumer)
|
|
24
|
-
runtime ← model factory + Agent assembly + task handoff/migration (createModel / createAgent / SdkAgent / HandoffService / HandoffGuardPlugin)
|
|
24
|
+
runtime ← model factory + Agent assembly + task handoff/migration (cancellable via `AbortSignal`; createModel / createAgent / SdkAgent / HandoffService / HandoffGuardPlugin)
|
|
25
25
|
scope ← Scope orchestration container + ScopePlugin contract + PermissionEvaluator / disclosure + plugins/ (5 self-contained capability sources: discovery + tools + plugin)
|
|
26
26
|
agent-plugins ← Agent plugins (autoMigrate, autoRefreshTools, contextGuard, unknownToolHint)
|
|
27
27
|
shared ← logging, errors
|
package/README.zh.md
CHANGED
|
@@ -20,7 +20,7 @@ Desktop ──┘ │
|
|
|
20
20
|
types ← 纯类型,零业务依赖(含 ToolEnv 工具环境)
|
|
21
21
|
config ← ConfigManager + constants:读写 config.json + 目录初始化 + 出厂默认
|
|
22
22
|
crud ← 能力实体 CRUD(Agent 定义等;会话/草稿已下放给各端自行持久化)
|
|
23
|
-
runtime ← 模型工厂 + Agent 组装 +
|
|
23
|
+
runtime ← 模型工厂 + Agent 组装 + 任务交接/迁移(可 `AbortSignal` 取消;createModel / createAgent / SdkAgent / HandoffService / HandoffGuardPlugin)
|
|
24
24
|
scope ← Scope 编排容器 + ScopePlugin 契约 + PermissionEvaluator / disclosure + plugins/(5 个自包含能力来源:发现 + 工具 + 插件)
|
|
25
25
|
agent-plugins ← Agent 插件(autoMigrate、autoRefreshTools、contextGuard、unknownToolHint)
|
|
26
26
|
shared ← 日志、错误
|
package/dist/index.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export { McpConnectionManager } from "./scope/plugins/mcp/McpConnectionManager.j
|
|
|
35
35
|
export type { McpConnectOptions } from "./scope/plugins/mcp/McpConnectionManager.js";
|
|
36
36
|
export { SdkCallbackTool } from "./scope/plugins/builtin/SdkCallbackTool.js";
|
|
37
37
|
export type { SdkCallbackToolOptions } from "./scope/plugins/builtin/SdkCallbackTool.js";
|
|
38
|
-
export { HandoffService } from "./runtime/HandoffService.js";
|
|
38
|
+
export { HandoffAbortedError, HandoffService } from "./runtime/HandoffService.js";
|
|
39
39
|
export type { HandoffContext, HandoffMode, HandoffParams, HandoffServiceOptions, MigrateParams, MigrationStrategy, } from "./runtime/HandoffService.js";
|
|
40
40
|
export { HandoffGuardPlugin } from "./runtime/HandoffGuardPlugin.js";
|
|
41
41
|
export { AutoMigratePlugin } from "./agent-plugins/AutoMigratePlugin.js";
|
package/dist/index.js
CHANGED
|
@@ -33,7 +33,7 @@ export { SdkAgent } from "./runtime/SdkAgent.js";
|
|
|
33
33
|
export { createAgent } from "./runtime/createAgent.js";
|
|
34
34
|
export { McpConnectionManager } from "./scope/plugins/mcp/McpConnectionManager.js";
|
|
35
35
|
export { SdkCallbackTool } from "./scope/plugins/builtin/SdkCallbackTool.js";
|
|
36
|
-
export { HandoffService } from "./runtime/HandoffService.js";
|
|
36
|
+
export { HandoffAbortedError, HandoffService } from "./runtime/HandoffService.js";
|
|
37
37
|
export { HandoffGuardPlugin } from "./runtime/HandoffGuardPlugin.js";
|
|
38
38
|
// 插件
|
|
39
39
|
export { AutoMigratePlugin } from "./agent-plugins/AutoMigratePlugin.js";
|
|
@@ -18,6 +18,18 @@ export type HandoffMode = "self" | "summary";
|
|
|
18
18
|
* - `prune`:物理剔除历史,只保留 `definition.messages`(系统提示等模板)+ 交接断点。
|
|
19
19
|
*/
|
|
20
20
|
export type MigrationStrategy = "omit" | "prune";
|
|
21
|
+
/**
|
|
22
|
+
* 交接被调用方取消(`AbortSignal` 触发)。
|
|
23
|
+
*
|
|
24
|
+
* 单独成类型而不是复用 `Error`,原因有两个:
|
|
25
|
+
* - 调用方(如 TUI)要把「用户按了 Ctrl+C」与「交接真的失败了」区分开 ——
|
|
26
|
+
* 前者应静默收尾并提示「已取消」,后者才该报错;
|
|
27
|
+
* - 它**不可降级**:`self` 被取消后绝不能退到 `summary` 再跑一遍。
|
|
28
|
+
* 把「用户中止」当成「模型没写出文档」,会白白多烧一轮全量输入。
|
|
29
|
+
*/
|
|
30
|
+
export declare class HandoffAbortedError extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
|
21
33
|
/**
|
|
22
34
|
* 一次交接的完整上下文 —— 贯穿「交接前 → 写文档 → 迁移后」,供钩子与调用方使用。
|
|
23
35
|
*/
|
|
@@ -86,6 +98,15 @@ export interface HandoffParams {
|
|
|
86
98
|
handoffTokens?: number;
|
|
87
99
|
/** 会话 id(可选,用于交接文件名,便于回溯) */
|
|
88
100
|
conversationId?: string;
|
|
101
|
+
/**
|
|
102
|
+
* 取消信号(可选)。触发后交接**立即停止且不降级**,抛 {@link HandoffAbortedError}。
|
|
103
|
+
*
|
|
104
|
+
* 两种执行者都要认它:
|
|
105
|
+
* - `summary`:直接作为 `chat.completions.create` 的 RequestOptions 传下去,
|
|
106
|
+
* 请求会真的被中断(否则该请求根本无从取消);
|
|
107
|
+
* - `self`:`agent.abort()` 不抛错,所以每个 `send()` 之后要主动检查 `aborted`。
|
|
108
|
+
*/
|
|
109
|
+
signal?: AbortSignal;
|
|
89
110
|
}
|
|
90
111
|
/** `migrate()` 参数 */
|
|
91
112
|
export interface MigrateParams extends HandoffParams {
|
|
@@ -174,6 +195,25 @@ export declare class HandoffService {
|
|
|
174
195
|
migrate(params: MigrateParams): Promise<HandoffContext>;
|
|
175
196
|
/** 串行化 + 防重入:交接与迁移共用同一把锁 */
|
|
176
197
|
private withBusy;
|
|
198
|
+
/**
|
|
199
|
+
* 把底层各种「取消」形态统一成 {@link HandoffAbortedError}。
|
|
200
|
+
*
|
|
201
|
+
* 取消可能从三处冒上来:`self` 回合后的主动检查(已是 HandoffAbortedError)、
|
|
202
|
+
* `summary` 请求被 abort 时 OpenAI SDK 抛的 `APIUserAbortError`、以及
|
|
203
|
+
* `readSkillContent` 这类前置 await 期间被取消。调用方只需要认一个类型。
|
|
204
|
+
*/
|
|
205
|
+
private runCatchingAbort;
|
|
206
|
+
/** 取消点检查:每跨过一个阶段就确认一次,避免取消后继续往下一步冲 */
|
|
207
|
+
private throwIfAborted;
|
|
208
|
+
private fileExists;
|
|
209
|
+
/**
|
|
210
|
+
* 取消后清掉半成品文档。
|
|
211
|
+
*
|
|
212
|
+
* 只删「本次新建」的文件:交接文件名含时间戳,正常不会撞已有文件,但调用方也
|
|
213
|
+
* 可以把 `handoffDir` 指向人工目录;不管怎样都不该删掉别人的文件。
|
|
214
|
+
* 留着半成品更糟 —— 下一轮接手方会把不完整的内容当成真实交接读进去。
|
|
215
|
+
*/
|
|
216
|
+
private discardPartialFile;
|
|
177
217
|
private runHandoff;
|
|
178
218
|
/** 自交接是否可用:需要 `write` 工具 + `send` 能力 */
|
|
179
219
|
private supportsSelfHandoff;
|
|
@@ -183,6 +223,12 @@ export declare class HandoffService {
|
|
|
183
223
|
* 失败判定(任一成立即视为失败,交由上层单向降级 summary):
|
|
184
224
|
* - 文件不存在 / 小于阈值;
|
|
185
225
|
* - 模型/工具链抛错(例如没有权限、被护栏中断)。
|
|
226
|
+
*
|
|
227
|
+
* **取消不算失败**:`signal` 已触发时抛 {@link HandoffAbortedError},既不走
|
|
228
|
+
* 「文件没写好→重试一次」也不降级 summary。 这条分支必须显式写:
|
|
229
|
+
* `agent.abort()` 不抛错(core 把消息标记为 Aborted 后正常返回),否则「用户
|
|
230
|
+
* 按了 Ctrl+C」会被下面的文件校验误判成「模型没写出文档」,于是重试一轮、再降级
|
|
231
|
+
* 一轮 —— 用户以为取消了,实际多烧了两轮全量输入。
|
|
186
232
|
*/
|
|
187
233
|
private runSelfHandoff;
|
|
188
234
|
/** 独立摘要:一次模型调用,用同一份 Skill 正文作为规范 */
|
|
@@ -5,6 +5,35 @@ import { getLogger } from "../shared/logger.js";
|
|
|
5
5
|
import { readSkill } from "../scope/plugins/skills/discover.js";
|
|
6
6
|
import { FALLBACK_HANDOFF_TEMPLATE, HANDOFF_SKILL_ID } from "../config/factory-skills.js";
|
|
7
7
|
import { HandoffGuardPlugin } from "./HandoffGuardPlugin.js";
|
|
8
|
+
/**
|
|
9
|
+
* 交接被调用方取消(`AbortSignal` 触发)。
|
|
10
|
+
*
|
|
11
|
+
* 单独成类型而不是复用 `Error`,原因有两个:
|
|
12
|
+
* - 调用方(如 TUI)要把「用户按了 Ctrl+C」与「交接真的失败了」区分开 ——
|
|
13
|
+
* 前者应静默收尾并提示「已取消」,后者才该报错;
|
|
14
|
+
* - 它**不可降级**:`self` 被取消后绝不能退到 `summary` 再跑一遍。
|
|
15
|
+
* 把「用户中止」当成「模型没写出文档」,会白白多烧一轮全量输入。
|
|
16
|
+
*/
|
|
17
|
+
export class HandoffAbortedError extends Error {
|
|
18
|
+
constructor() {
|
|
19
|
+
super("交接已取消");
|
|
20
|
+
this.name = "HandoffAbortedError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 错误是否来自取消。
|
|
25
|
+
*
|
|
26
|
+
* 两条路径都要认:
|
|
27
|
+
* - 调用方传了 `signal` 且已 `aborted`(`agent.abort()` 就是这种情况 ——
|
|
28
|
+
* 它**不抛错**,core 层把消息标记成 Aborted 后正常返回);
|
|
29
|
+
* - OpenAI SDK 在请求被 abort 时抛的 `APIUserAbortError`(`name` 为 `AbortError`)。
|
|
30
|
+
*/
|
|
31
|
+
function isAbortError(err, signal) {
|
|
32
|
+
if (signal?.aborted)
|
|
33
|
+
return true;
|
|
34
|
+
const name = err?.name;
|
|
35
|
+
return name === "AbortError" || name === "APIUserAbortError";
|
|
36
|
+
}
|
|
8
37
|
/**
|
|
9
38
|
* 任务交接服务。
|
|
10
39
|
*
|
|
@@ -145,7 +174,7 @@ export class HandoffService {
|
|
|
145
174
|
* 用户可继续让 agent 打磨这份文档(重写同一路径即可)。
|
|
146
175
|
*/
|
|
147
176
|
async handoff(params) {
|
|
148
|
-
return this.withBusy(() => this.
|
|
177
|
+
return this.withBusy(() => this.runCatchingAbort(params, false));
|
|
149
178
|
}
|
|
150
179
|
/**
|
|
151
180
|
* 交接 + 开新会话(`/migrate`、自动迁移)。
|
|
@@ -156,7 +185,7 @@ export class HandoffService {
|
|
|
156
185
|
*/
|
|
157
186
|
async migrate(params) {
|
|
158
187
|
return this.withBusy(async () => {
|
|
159
|
-
const ctx = await this.
|
|
188
|
+
const ctx = await this.runCatchingAbort(params, true);
|
|
160
189
|
const { agent } = ctx;
|
|
161
190
|
const strategy = params.strategy ?? this.defaultStrategy;
|
|
162
191
|
const breakpointMessages = HandoffService.createPostMessages(ctx.handoffPath);
|
|
@@ -193,8 +222,58 @@ export class HandoffService {
|
|
|
193
222
|
this._busy = false;
|
|
194
223
|
}
|
|
195
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* 把底层各种「取消」形态统一成 {@link HandoffAbortedError}。
|
|
227
|
+
*
|
|
228
|
+
* 取消可能从三处冒上来:`self` 回合后的主动检查(已是 HandoffAbortedError)、
|
|
229
|
+
* `summary` 请求被 abort 时 OpenAI SDK 抛的 `APIUserAbortError`、以及
|
|
230
|
+
* `readSkillContent` 这类前置 await 期间被取消。调用方只需要认一个类型。
|
|
231
|
+
*/
|
|
232
|
+
async runCatchingAbort(params, migrating) {
|
|
233
|
+
try {
|
|
234
|
+
return await this.runHandoff(params, migrating);
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
if (err instanceof HandoffAbortedError)
|
|
238
|
+
throw err;
|
|
239
|
+
if (isAbortError(err, params.signal))
|
|
240
|
+
throw new HandoffAbortedError();
|
|
241
|
+
throw err;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/** 取消点检查:每跨过一个阶段就确认一次,避免取消后继续往下一步冲 */
|
|
245
|
+
throwIfAborted(signal) {
|
|
246
|
+
if (signal?.aborted)
|
|
247
|
+
throw new HandoffAbortedError();
|
|
248
|
+
}
|
|
249
|
+
async fileExists(path) {
|
|
250
|
+
try {
|
|
251
|
+
await fs.access(path);
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 取消后清掉半成品文档。
|
|
260
|
+
*
|
|
261
|
+
* 只删「本次新建」的文件:交接文件名含时间戳,正常不会撞已有文件,但调用方也
|
|
262
|
+
* 可以把 `handoffDir` 指向人工目录;不管怎样都不该删掉别人的文件。
|
|
263
|
+
* 留着半成品更糟 —— 下一轮接手方会把不完整的内容当成真实交接读进去。
|
|
264
|
+
*/
|
|
265
|
+
async discardPartialFile(path, existedBefore) {
|
|
266
|
+
if (existedBefore)
|
|
267
|
+
return;
|
|
268
|
+
try {
|
|
269
|
+
await fs.unlink(path);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
/* 文件本就没写出来:忽略 */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
196
275
|
async runHandoff(params, migrating) {
|
|
197
|
-
const { agent, promptTokens, handoffTokens, conversationId } = params;
|
|
276
|
+
const { agent, promptTokens, handoffTokens, conversationId, signal } = params;
|
|
198
277
|
const requestedMode = params.mode ?? this.defaultMode;
|
|
199
278
|
const handoffPath = join(this.handoffDirPath, HandoffService.createHandoffFileName(new Date(), conversationId));
|
|
200
279
|
const model = this.model ?? agent.model;
|
|
@@ -213,6 +292,7 @@ export class HandoffService {
|
|
|
213
292
|
historyText: "",
|
|
214
293
|
};
|
|
215
294
|
await this.safeHook(this.onBeforeHandoff, beforeCtx);
|
|
295
|
+
this.throwIfAborted(signal);
|
|
216
296
|
// ---- 执行者:self 优先,失败/不可用则单向降级 summary ----
|
|
217
297
|
let mode = requestedMode;
|
|
218
298
|
let handoffDoc;
|
|
@@ -222,7 +302,7 @@ export class HandoffService {
|
|
|
222
302
|
mode = "summary";
|
|
223
303
|
}
|
|
224
304
|
if (mode === "self") {
|
|
225
|
-
const ok = await this.runSelfHandoff(agent, handoffPath);
|
|
305
|
+
const ok = await this.runSelfHandoff(agent, handoffPath, signal);
|
|
226
306
|
if (!ok) {
|
|
227
307
|
this.logger.warn("[handoff] 自交接失败,降级为独立摘要模式");
|
|
228
308
|
mode = "summary";
|
|
@@ -230,9 +310,11 @@ export class HandoffService {
|
|
|
230
310
|
}
|
|
231
311
|
if (mode === "summary") {
|
|
232
312
|
historyText = HandoffService.serializeMessages(agent.messages.filter((m) => !m.omit));
|
|
233
|
-
handoffDoc = await this.generateSummary(agent, historyText, handoffPath);
|
|
313
|
+
handoffDoc = await this.generateSummary(agent, historyText, handoffPath, signal);
|
|
234
314
|
await this.writeHandoffFile(handoffPath, handoffDoc);
|
|
235
315
|
}
|
|
316
|
+
// 最后一道取消点:即使两步都跑完,只要期间被取消就不能算成功
|
|
317
|
+
this.throwIfAborted(signal);
|
|
236
318
|
const ctx = {
|
|
237
319
|
agent,
|
|
238
320
|
mode,
|
|
@@ -261,11 +343,19 @@ export class HandoffService {
|
|
|
261
343
|
* 失败判定(任一成立即视为失败,交由上层单向降级 summary):
|
|
262
344
|
* - 文件不存在 / 小于阈值;
|
|
263
345
|
* - 模型/工具链抛错(例如没有权限、被护栏中断)。
|
|
346
|
+
*
|
|
347
|
+
* **取消不算失败**:`signal` 已触发时抛 {@link HandoffAbortedError},既不走
|
|
348
|
+
* 「文件没写好→重试一次」也不降级 summary。 这条分支必须显式写:
|
|
349
|
+
* `agent.abort()` 不抛错(core 把消息标记为 Aborted 后正常返回),否则「用户
|
|
350
|
+
* 按了 Ctrl+C」会被下面的文件校验误判成「模型没写出文档」,于是重试一轮、再降级
|
|
351
|
+
* 一轮 —— 用户以为取消了,实际多烧了两轮全量输入。
|
|
264
352
|
*/
|
|
265
|
-
async runSelfHandoff(agent, handoffPath) {
|
|
353
|
+
async runSelfHandoff(agent, handoffPath, signal) {
|
|
266
354
|
const guard = this.ensureGuard(agent);
|
|
267
355
|
const skillContent = await this.readSkillContent();
|
|
268
356
|
const cwd = agent.scope?.cwd ?? process.cwd();
|
|
357
|
+
// 取消时只删本次新建的文件,不碰本已存在的同名文件
|
|
358
|
+
const existedBefore = await this.fileExists(handoffPath);
|
|
269
359
|
/*
|
|
270
360
|
* 清空 lastUsage 再开跑:
|
|
271
361
|
* - 护栏(ContextGuardPlugin)在每轮请求前用 lastUsage 判定严重超限,
|
|
@@ -281,6 +371,8 @@ export class HandoffService {
|
|
|
281
371
|
handoffPath,
|
|
282
372
|
toolsAvailable: true,
|
|
283
373
|
}));
|
|
374
|
+
// `agent.abort()` 不抛错,必须主动检查(见方法注释)
|
|
375
|
+
this.throwIfAborted(signal);
|
|
284
376
|
if (await this.isHandoffFileReady(handoffPath))
|
|
285
377
|
return true;
|
|
286
378
|
this.logger.warn(`[handoff] 交接文档缺失或过小,重试一次:${handoffPath}`);
|
|
@@ -288,9 +380,14 @@ export class HandoffService {
|
|
|
288
380
|
`交接文档不存在或内容过少,无法交付:${handoffPath}`,
|
|
289
381
|
"请立即用 write 工具把**完整**交接文档写入该路径(路径原样使用),不要做其他任何事。",
|
|
290
382
|
].join("\n"));
|
|
383
|
+
this.throwIfAborted(signal);
|
|
291
384
|
return await this.isHandoffFileReady(handoffPath);
|
|
292
385
|
}
|
|
293
386
|
catch (err) {
|
|
387
|
+
if (isAbortError(err, signal)) {
|
|
388
|
+
await this.discardPartialFile(handoffPath, existedBefore);
|
|
389
|
+
throw new HandoffAbortedError();
|
|
390
|
+
}
|
|
294
391
|
this.logger.error(`[handoff] 自交接回合失败:${err?.message ?? err}`);
|
|
295
392
|
return false;
|
|
296
393
|
}
|
|
@@ -299,11 +396,14 @@ export class HandoffService {
|
|
|
299
396
|
}
|
|
300
397
|
}
|
|
301
398
|
/** 独立摘要:一次模型调用,用同一份 Skill 正文作为规范 */
|
|
302
|
-
async generateSummary(agent, historyText, handoffPath) {
|
|
399
|
+
async generateSummary(agent, historyText, handoffPath, signal) {
|
|
303
400
|
const client = this.client ?? agent.client;
|
|
304
401
|
const model = this.model ?? agent.model;
|
|
305
402
|
const modelConfig = this.modelConfig ?? agent.modelConfig;
|
|
306
403
|
const skillContent = await this.readSkillContent();
|
|
404
|
+
this.throwIfAborted(signal);
|
|
405
|
+
// `signal` 走 RequestOptions(第二参数),与 `core/Agent.ts` 调用模型时一致:
|
|
406
|
+
// 放在 body 里不会被 OpenAI SDK 当成取消信号。
|
|
307
407
|
const response = await client.chat.completions.create({
|
|
308
408
|
model,
|
|
309
409
|
...(modelConfig ?? {}),
|
|
@@ -318,7 +418,7 @@ export class HandoffService {
|
|
|
318
418
|
},
|
|
319
419
|
{ role: "user", content: historyText },
|
|
320
420
|
],
|
|
321
|
-
});
|
|
421
|
+
}, signal ? { signal } : undefined);
|
|
322
422
|
const handoffDoc = response.choices[0]?.message?.content;
|
|
323
423
|
if (!handoffDoc || !handoffDoc.trim()) {
|
|
324
424
|
throw new Error("未能生成任务交接文档");
|