@keyou007/agent-bot 0.1.19-alpha.1 → 0.1.19
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 +4 -0
- package/dist/codex/CodexRuntime.js +59 -1
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +6 -3
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/docs/technical-reference.md +1 -1
- package/docs/technical-reference.zh.md +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/codex/CodexRuntime.ts +79 -1
- package/src/proxy/ProxySessionController.ts +9 -3
- package/src/runtime/types.ts +1 -0
|
@@ -261,7 +261,7 @@ Starting a Feishu thread from a mapped user message, progress card, or final res
|
|
|
261
261
|
|
|
262
262
|
`/forkgroup` has thread-aware source selection. An unbound thread, or a bound thread task with no completed turn of its own, forks directly from the thread's original anchor turn without creating an intermediate thread task. Once the thread task has completed a turn, `/forkgroup` uses its latest locally persisted completed turn. A newer active turn does not block the command and is excluded from the fork point.
|
|
263
263
|
|
|
264
|
-
Agent Bot sends the experimental `excludeTurns: true` field on every `thread/fork` request. This suppresses populated `thread.turns` in the response without changing the history copied into the fork. The App Server connection enables `experimentalApi`; no user-facing command option is required. If an older App Server explicitly rejects
|
|
264
|
+
Before forking a task that has never been bound to Agent Bot, Agent Bot reads metadata without Turns and pages backward through the lightweight `summary` Turn view only until it finds the latest completed Turn. It does not load the task's complete tool history as a Fork prerequisite. Agent Bot then sends the experimental `excludeTurns: true` field on every `thread/fork` request. This suppresses populated `thread.turns` in the response without changing the history copied into the fork. The App Server connection enables `experimentalApi`; no user-facing command option is required. If an older App Server explicitly rejects the lightweight Turn-list method or the `excludeTurns` field, Agent Bot falls back to the compatible complete-history read or Fork request. Timeouts, disconnects, and unrelated Fork errors are never retried because a timed-out Fork request may already have created a branch.
|
|
265
265
|
|
|
266
266
|
The new group's welcome message reports the persisted fork settings: Provider, model, reasoning effort, and permission type. Permission type is rendered as automatic execution or confirmation before execution.
|
|
267
267
|
|
|
@@ -261,7 +261,7 @@ AGENT_BOT_LARK_USER_OPEN_ID=<已保存的授权用户 open_id,可用时提供>
|
|
|
261
261
|
|
|
262
262
|
`/forkgroup` 会根据话题状态选择来源。话题尚未绑定任务,或者已绑定任务但尚未完成过自己的 turn 时,直接从话题原始锚点 turn fork,不创建中间话题任务。话题任务完成过 turn 后,使用本地持久化的最近完成 turn;更晚的执行中 turn 不会阻塞命令,也不会成为 fork 点。
|
|
263
263
|
|
|
264
|
-
Agent Bot
|
|
264
|
+
Fork 从未绑定过 Agent Bot 的任务前,Agent Bot 会先读取不含 Turns 的元数据,再通过轻量的 `summary` Turn 视图从新到旧分页,只读到最近一个已完成 Turn,不再把完整工具历史作为 Fork 前置条件。随后每个 `thread/fork` 请求仍默认发送实验性的 `excludeTurns: true`;它只阻止响应填充 `thread.turns`,不会改变复制到分支中的历史。App Server 连接已启用 `experimentalApi`,不需要用户提供命令参数。如果旧版 App Server 明确不支持轻量 Turn 列表或 `excludeTurns` 字段,Agent Bot 会分别回退到兼容的完整历史读取或 Fork 请求。超时、断连和无关的 Fork 错误绝不重试,因为超时的 Fork 请求可能已经创建分支。
|
|
265
265
|
|
|
266
266
|
新群欢迎消息会显示分支任务持久化后的 Provider、模型、思考强度和权限类型;权限类型显示为自动执行或执行前确认。
|
|
267
267
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyou007/agent-bot",
|
|
3
|
-
"version": "0.1.19
|
|
3
|
+
"version": "0.1.19",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@keyou007/agent-bot",
|
|
9
|
-
"version": "0.1.19
|
|
9
|
+
"version": "0.1.19",
|
|
10
10
|
"license": "GPL-3.0-only",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "1.2.1",
|
package/package.json
CHANGED
|
@@ -42,6 +42,8 @@ const SESSION_REQUEST_TIMEOUT_MS = 60_000;
|
|
|
42
42
|
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
43
43
|
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
44
44
|
const THREAD_TURN_PAGE_SIZE = 100;
|
|
45
|
+
const FORK_SOURCE_TURN_PAGE_SIZE = 20;
|
|
46
|
+
const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
|
|
45
47
|
const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"] as const;
|
|
46
48
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
47
49
|
// Wait for its response; connection closure still rejects the request.
|
|
@@ -305,7 +307,37 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
305
307
|
async readRemoteSession(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
306
308
|
const client = await this.client();
|
|
307
309
|
const response = await this.readThreadWithTurns(client, remoteSessionId);
|
|
308
|
-
|
|
310
|
+
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary(response.thread));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async readRemoteForkSource(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
314
|
+
const client = await this.client();
|
|
315
|
+
const metadata = await client.request<ThreadReadResponse>(
|
|
316
|
+
"thread/read",
|
|
317
|
+
{ threadId: remoteSessionId, includeTurns: false },
|
|
318
|
+
FORK_SOURCE_REQUEST_TIMEOUT_MS,
|
|
319
|
+
);
|
|
320
|
+
let turns: CodexTurnSnapshot[];
|
|
321
|
+
try {
|
|
322
|
+
turns = await this.listRecentThreadTurnsThroughCompleted(client, remoteSessionId);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
if (!isUnsupportedThreadTurnsListError(error)) throw error;
|
|
325
|
+
this.logger.warn(
|
|
326
|
+
{ error, remoteSessionId },
|
|
327
|
+
"App Server does not support lightweight Fork source reads; falling back to complete thread history.",
|
|
328
|
+
);
|
|
329
|
+
return this.readRemoteSession(remoteSessionId);
|
|
330
|
+
}
|
|
331
|
+
return this.decorateRemoteSession(remoteSessionId, remoteSessionSummary({
|
|
332
|
+
...metadata.thread,
|
|
333
|
+
turns,
|
|
334
|
+
}));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
private async decorateRemoteSession(
|
|
338
|
+
remoteSessionId: string,
|
|
339
|
+
summary: RemoteSessionSummary,
|
|
340
|
+
): Promise<RemoteSessionSummary> {
|
|
309
341
|
const activeThreads = await this.localActivityDetector?.activeThreads([remoteSessionId]);
|
|
310
342
|
const activeSummary = activeThreads ? markLocallyDetectedActive(summary, activeThreads) : summary;
|
|
311
343
|
const settings = await this.localActivityDetector?.threadSettings([remoteSessionId]);
|
|
@@ -315,6 +347,46 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
315
347
|
};
|
|
316
348
|
}
|
|
317
349
|
|
|
350
|
+
private async listRecentThreadTurnsThroughCompleted(
|
|
351
|
+
client: AppServerClient,
|
|
352
|
+
remoteSessionId: string,
|
|
353
|
+
): Promise<CodexTurnSnapshot[]> {
|
|
354
|
+
const turns: CodexTurnSnapshot[] = [];
|
|
355
|
+
const seenCursors = new Set<string>();
|
|
356
|
+
let cursor: string | undefined;
|
|
357
|
+
do {
|
|
358
|
+
const response = await client.request<ThreadTurnsListResponse>(
|
|
359
|
+
"thread/turns/list",
|
|
360
|
+
{
|
|
361
|
+
threadId: remoteSessionId,
|
|
362
|
+
...(cursor ? { cursor } : {}),
|
|
363
|
+
limit: FORK_SOURCE_TURN_PAGE_SIZE,
|
|
364
|
+
sortDirection: "desc",
|
|
365
|
+
itemsView: "summary",
|
|
366
|
+
},
|
|
367
|
+
FORK_SOURCE_REQUEST_TIMEOUT_MS,
|
|
368
|
+
);
|
|
369
|
+
turns.push(...response.data);
|
|
370
|
+
const completedIndex = turns.findIndex((turn) => turn.status === "completed");
|
|
371
|
+
if (completedIndex >= 0) {
|
|
372
|
+
turns.splice(completedIndex + 1);
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
const nextCursor = response.nextCursor ?? undefined;
|
|
376
|
+
if (!nextCursor) break;
|
|
377
|
+
if (seenCursors.has(nextCursor)) {
|
|
378
|
+
this.logger.warn(
|
|
379
|
+
{ remoteSessionId, cursor: nextCursor },
|
|
380
|
+
"App Server repeated a Fork source Turn cursor; stopping pagination.",
|
|
381
|
+
);
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
seenCursors.add(nextCursor);
|
|
385
|
+
cursor = nextCursor;
|
|
386
|
+
} while (cursor);
|
|
387
|
+
return turns.reverse();
|
|
388
|
+
}
|
|
389
|
+
|
|
318
390
|
private async readThreadWithTurns(
|
|
319
391
|
client: AppServerClient,
|
|
320
392
|
remoteSessionId: string,
|
|
@@ -1229,6 +1301,12 @@ function isPaginatedThreadReadError(error: unknown): boolean {
|
|
|
1229
1301
|
return /paginated threads?[^\n]*do(?:es)? not support[^\n]*thread\/read[^\n]*includeTurns\s*=\s*true/i.test(details);
|
|
1230
1302
|
}
|
|
1231
1303
|
|
|
1304
|
+
function isUnsupportedThreadTurnsListError(error: unknown): boolean {
|
|
1305
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/turns/list") return false;
|
|
1306
|
+
return error.code === -32601
|
|
1307
|
+
|| /method not found|unknown method|unsupported|not supported/i.test(appServerErrorDetails(error));
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1232
1310
|
function isUnmaterializedThreadReadError(error: unknown): boolean {
|
|
1233
1311
|
const details = appServerErrorDetails(error);
|
|
1234
1312
|
return /thread[^\n]*(?:not materialized|not loaded)[^\n]*includeTurns[^\n]*(?:unavailable|before first user message)/i
|
|
@@ -3041,7 +3041,7 @@ export class ProxySessionController {
|
|
|
3041
3041
|
runtime = this.runtimes.forAgent(agentName);
|
|
3042
3042
|
} else {
|
|
3043
3043
|
if (!taskId) throw new Error("缺少要 fork 的 App Server 任务 ID。");
|
|
3044
|
-
const resolved = await this.resolveRemoteCodexSession(taskId);
|
|
3044
|
+
const resolved = await this.resolveRemoteCodexSession(taskId, { forFork: true });
|
|
3045
3045
|
agentName = resolved.agentName;
|
|
3046
3046
|
runtime = resolved.runtime;
|
|
3047
3047
|
remote = resolved.remote;
|
|
@@ -6218,7 +6218,10 @@ export class ProxySessionController {
|
|
|
6218
6218
|
: this.store.findSessionByRemoteSessionId(reference, contextKey);
|
|
6219
6219
|
}
|
|
6220
6220
|
|
|
6221
|
-
private async resolveRemoteCodexSession(
|
|
6221
|
+
private async resolveRemoteCodexSession(
|
|
6222
|
+
reference: string,
|
|
6223
|
+
options: { forFork?: boolean } = {},
|
|
6224
|
+
): Promise<AgentRemoteSession> {
|
|
6222
6225
|
const scoped = parseRemoteSessionReference(reference);
|
|
6223
6226
|
const candidates = scoped
|
|
6224
6227
|
? [[scoped.agentName, this.runtimes.forAgent(scoped.agentName)] as const]
|
|
@@ -6229,10 +6232,13 @@ export class ProxySessionController {
|
|
|
6229
6232
|
if (runtime.kind !== "codex" || !runtime.readRemoteSession) {
|
|
6230
6233
|
throw new Error(`Agent ${agentName} 不支持读取远端任务。`);
|
|
6231
6234
|
}
|
|
6235
|
+
const remoteSessionId = scoped?.remoteSessionId ?? reference;
|
|
6232
6236
|
return {
|
|
6233
6237
|
agentName,
|
|
6234
6238
|
runtime,
|
|
6235
|
-
remote:
|
|
6239
|
+
remote: options.forFork && runtime.readRemoteForkSource
|
|
6240
|
+
? await runtime.readRemoteForkSource(remoteSessionId)
|
|
6241
|
+
: await runtime.readRemoteSession(remoteSessionId),
|
|
6236
6242
|
} satisfies AgentRemoteSession;
|
|
6237
6243
|
}));
|
|
6238
6244
|
const matches = reads
|
package/src/runtime/types.ts
CHANGED
|
@@ -230,6 +230,7 @@ export interface AgentRuntime {
|
|
|
230
230
|
readSessionMetadata(remoteSessionId: string): Promise<RuntimeSessionMetadata>;
|
|
231
231
|
listRemoteSessions?(input?: { searchTerm?: string; cursor?: string; limit?: number }): Promise<RemoteSessionPage>;
|
|
232
232
|
readRemoteSession?(remoteSessionId: string): Promise<RemoteSessionSummary>;
|
|
233
|
+
readRemoteForkSource?(remoteSessionId: string): Promise<RemoteSessionSummary>;
|
|
233
234
|
inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
|
|
234
235
|
synchronizeSession(sessionId: string): Promise<RuntimeSession>;
|
|
235
236
|
startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
|