@keyou007/agent-bot 0.1.19-alpha.0 → 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 +9 -0
- package/README.md +3 -0
- package/README.zh.md +3 -0
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +2 -0
- package/dist/cli/help.js.map +1 -1
- package/dist/cli.js +20 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +48 -0
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +94 -3
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +5 -0
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +83 -0
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/MergedForwardMessage.js +6 -1
- package/dist/feishu/MergedForwardMessage.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +455 -7
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/state/StateStore.js +10 -0
- package/dist/state/StateStore.js.map +1 -1
- package/docs/technical-reference.md +2 -2
- package/docs/technical-reference.zh.md +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +3 -0
- package/src/cli/controlProtocol.ts +8 -0
- package/src/cli/help.ts +2 -0
- package/src/cli.ts +33 -0
- package/src/codex/CodexProcessManager.ts +43 -0
- package/src/codex/CodexRuntime.ts +112 -3
- package/src/commands/CommandRouter.ts +4 -0
- package/src/commands/commandTypes.ts +1 -0
- package/src/feishu/CardRenderer.ts +96 -0
- package/src/feishu/MergedForwardMessage.ts +5 -1
- package/src/index.ts +2 -0
- package/src/proxy/ProxySessionController.ts +553 -9
- package/src/runtime/types.ts +7 -0
- package/src/state/StateStore.ts +11 -0
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
RuntimeEvent,
|
|
18
18
|
RuntimeExecutionSettings,
|
|
19
19
|
RuntimePrompt,
|
|
20
|
+
RuntimeReleaseResult,
|
|
20
21
|
RuntimeSession,
|
|
21
22
|
RuntimeSessionMetadata,
|
|
22
23
|
} from "../runtime/types.js";
|
|
@@ -41,6 +42,9 @@ const SESSION_REQUEST_TIMEOUT_MS = 60_000;
|
|
|
41
42
|
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
42
43
|
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
43
44
|
const THREAD_TURN_PAGE_SIZE = 100;
|
|
45
|
+
const FORK_SOURCE_TURN_PAGE_SIZE = 20;
|
|
46
|
+
const FORK_SOURCE_REQUEST_TIMEOUT_MS = 15_000;
|
|
47
|
+
const USER_RESUMABLE_THREAD_SOURCE_KINDS = ["cli", "vscode", "exec", "appServer"] as const;
|
|
44
48
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
45
49
|
// Wait for its response; connection closure still rejects the request.
|
|
46
50
|
const FORK_REQUEST_TIMEOUT_MS = 0;
|
|
@@ -61,6 +65,7 @@ export interface AppServerClientProvider {
|
|
|
61
65
|
getProcessInfo?(): AgentProcessInfo;
|
|
62
66
|
getCodexHome?(): string;
|
|
63
67
|
onDisconnect?(listener: (error: Error) => void): () => void;
|
|
68
|
+
release?(): Promise<void>;
|
|
64
69
|
close(): void;
|
|
65
70
|
}
|
|
66
71
|
|
|
@@ -91,6 +96,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
91
96
|
private readonly localActivityDetector?: CodexLocalActivityDetector;
|
|
92
97
|
private readonly codexHome?: string;
|
|
93
98
|
private threadListSortKey: ThreadListSortKey = "recency_at";
|
|
99
|
+
private releaseInFlight?: Promise<RuntimeReleaseResult>;
|
|
94
100
|
|
|
95
101
|
constructor(
|
|
96
102
|
private readonly provider: AppServerClientProvider,
|
|
@@ -290,6 +296,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
290
296
|
limit: input.limit ?? 20,
|
|
291
297
|
sortKey,
|
|
292
298
|
sortDirection: "desc",
|
|
299
|
+
sourceKinds: USER_RESUMABLE_THREAD_SOURCE_KINDS,
|
|
293
300
|
archived: false,
|
|
294
301
|
searchTerm: input.searchTerm,
|
|
295
302
|
},
|
|
@@ -300,7 +307,37 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
300
307
|
async readRemoteSession(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
301
308
|
const client = await this.client();
|
|
302
309
|
const response = await this.readThreadWithTurns(client, remoteSessionId);
|
|
303
|
-
|
|
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> {
|
|
304
341
|
const activeThreads = await this.localActivityDetector?.activeThreads([remoteSessionId]);
|
|
305
342
|
const activeSummary = activeThreads ? markLocallyDetectedActive(summary, activeThreads) : summary;
|
|
306
343
|
const settings = await this.localActivityDetector?.threadSettings([remoteSessionId]);
|
|
@@ -310,6 +347,46 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
310
347
|
};
|
|
311
348
|
}
|
|
312
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
|
+
|
|
313
390
|
private async readThreadWithTurns(
|
|
314
391
|
client: AppServerClient,
|
|
315
392
|
remoteSessionId: string,
|
|
@@ -602,6 +679,29 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
602
679
|
return () => this.listeners.delete(listener);
|
|
603
680
|
}
|
|
604
681
|
|
|
682
|
+
async release(options: { force?: boolean } = {}): Promise<RuntimeReleaseResult> {
|
|
683
|
+
if (this.releaseInFlight) return this.releaseInFlight;
|
|
684
|
+
const activeSessionIds = [...this.sessions.values()]
|
|
685
|
+
.filter((session) => Boolean(session.activeTurnId))
|
|
686
|
+
.map((session) => session.localSessionId);
|
|
687
|
+
if (activeSessionIds.length > 0 && options.force !== true) {
|
|
688
|
+
return { status: "busy", activeSessionIds };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
const operation = (async (): Promise<RuntimeReleaseResult> => {
|
|
692
|
+
this.handleDisconnect(new Error("App Server released by user."), true);
|
|
693
|
+
if (this.provider.release) await this.provider.release();
|
|
694
|
+
else this.provider.close();
|
|
695
|
+
return { status: "released" };
|
|
696
|
+
})();
|
|
697
|
+
this.releaseInFlight = operation;
|
|
698
|
+
try {
|
|
699
|
+
return await operation;
|
|
700
|
+
} finally {
|
|
701
|
+
if (this.releaseInFlight === operation) this.releaseInFlight = undefined;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
605
705
|
close(): void {
|
|
606
706
|
this.unsubscribe?.();
|
|
607
707
|
this.unsubscribeDisconnect?.();
|
|
@@ -613,6 +713,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
613
713
|
}
|
|
614
714
|
|
|
615
715
|
private async client(): Promise<AppServerClient> {
|
|
716
|
+
if (this.releaseInFlight) await this.releaseInFlight;
|
|
616
717
|
const client = await this.provider.getClient();
|
|
617
718
|
if (client !== this.attachedClient) this.attachClient(client);
|
|
618
719
|
return client;
|
|
@@ -1012,7 +1113,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1012
1113
|
for (const listener of this.listeners) listener(event);
|
|
1013
1114
|
}
|
|
1014
1115
|
|
|
1015
|
-
private handleDisconnect(error: Error): void {
|
|
1116
|
+
private handleDisconnect(error: Error, intentionalRelease = false): void {
|
|
1016
1117
|
this.unsubscribe?.();
|
|
1017
1118
|
this.unsubscribe = undefined;
|
|
1018
1119
|
this.attachedClient = undefined;
|
|
@@ -1027,7 +1128,9 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
1027
1128
|
type: "turn_failed",
|
|
1028
1129
|
sessionId: session.localSessionId,
|
|
1029
1130
|
turnId,
|
|
1030
|
-
message:
|
|
1131
|
+
message: intentionalRelease
|
|
1132
|
+
? "Task interrupted because Agent Bot released the App Server."
|
|
1133
|
+
: `App Server disconnected: ${error.message}`,
|
|
1031
1134
|
});
|
|
1032
1135
|
}
|
|
1033
1136
|
for (const [requestId, pending] of this.approvals) {
|
|
@@ -1198,6 +1301,12 @@ function isPaginatedThreadReadError(error: unknown): boolean {
|
|
|
1198
1301
|
return /paginated threads?[^\n]*do(?:es)? not support[^\n]*thread\/read[^\n]*includeTurns\s*=\s*true/i.test(details);
|
|
1199
1302
|
}
|
|
1200
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
|
+
|
|
1201
1310
|
function isUnmaterializedThreadReadError(error: unknown): boolean {
|
|
1202
1311
|
const details = appServerErrorDetails(error);
|
|
1203
1312
|
return /thread[^\n]*(?:not materialized|not loaded)[^\n]*includeTurns[^\n]*(?:unavailable|before first user message)/i
|
|
@@ -18,6 +18,7 @@ export const COMMAND_NAMES = [
|
|
|
18
18
|
"permissions",
|
|
19
19
|
"provider",
|
|
20
20
|
"queue",
|
|
21
|
+
"release",
|
|
21
22
|
"restart",
|
|
22
23
|
"sessions",
|
|
23
24
|
"status",
|
|
@@ -114,6 +115,9 @@ export class CommandRouter {
|
|
|
114
115
|
if (args.length === 0) return { type: "restart" };
|
|
115
116
|
if (args.length === 1 && args[0] === "--force") return { type: "restart", force: true };
|
|
116
117
|
throw new Error("/restart 只接受一个可选的 --force 参数。");
|
|
118
|
+
case "release":
|
|
119
|
+
if (args.length > 0) throw new Error("/release 不接受参数。");
|
|
120
|
+
return { type: "release" };
|
|
117
121
|
case "mute": {
|
|
118
122
|
if (args.length === 0 || (args.length === 1 && args[0]!.toLowerCase() === "on")) {
|
|
119
123
|
return { type: "mute", enabled: true };
|
|
@@ -19,6 +19,7 @@ export type Command =
|
|
|
19
19
|
| { type: "goal"; action: "set" | "edit"; objective: string }
|
|
20
20
|
| { type: "goal"; action: "pause" | "resume" | "clear" }
|
|
21
21
|
| { type: "restart"; force?: boolean }
|
|
22
|
+
| { type: "release" }
|
|
22
23
|
| { type: "mute"; enabled: boolean }
|
|
23
24
|
| { type: "turns" }
|
|
24
25
|
| { type: "model" }
|
|
@@ -275,6 +275,17 @@ export interface ThreadWriterConflictCardView {
|
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
export interface AppServerReleaseCardView {
|
|
279
|
+
status: "waiting" | "releasing" | "released" | "cancelled" | "failed";
|
|
280
|
+
contextKey: string;
|
|
281
|
+
scheduleId: number;
|
|
282
|
+
agentName: string;
|
|
283
|
+
blockingTaskCount?: number;
|
|
284
|
+
releaseInSeconds?: number;
|
|
285
|
+
forced?: boolean;
|
|
286
|
+
error?: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
278
289
|
export class CardRenderer {
|
|
279
290
|
private readonly thinkingCardLayout: ThinkingCardLayout;
|
|
280
291
|
|
|
@@ -371,6 +382,91 @@ export class CardRenderer {
|
|
|
371
382
|
);
|
|
372
383
|
}
|
|
373
384
|
|
|
385
|
+
renderAppServerRelease(view: AppServerReleaseCardView): Record<string, unknown> {
|
|
386
|
+
const agent = inlineCode(view.agentName);
|
|
387
|
+
if (view.status === "released") {
|
|
388
|
+
return sectionCard("任务已释放", [markdown([
|
|
389
|
+
`**Agent**:${agent}`,
|
|
390
|
+
view.forced ? "执行中的任务已中断。" : undefined,
|
|
391
|
+
"> 请在 Codex Desktop 点击 **Retry**。",
|
|
392
|
+
].filter((line): line is string => Boolean(line)).join("\n"))], "green");
|
|
393
|
+
}
|
|
394
|
+
if (view.status === "cancelled") {
|
|
395
|
+
return sectionCard("已取消释放", [markdown(`**Agent**:${agent}\n> Agent Bot 将继续保持当前任务连接。`)], "grey");
|
|
396
|
+
}
|
|
397
|
+
if (view.status === "failed") {
|
|
398
|
+
return sectionCard("释放失败", [markdown([
|
|
399
|
+
`**Agent**:${agent}`,
|
|
400
|
+
view.error ? truncateText(view.error, 500) : "无法释放 App Server。",
|
|
401
|
+
].join("\n"))], "red");
|
|
402
|
+
}
|
|
403
|
+
if (view.status === "releasing") {
|
|
404
|
+
return sectionCard("正在释放", [markdown(`**Agent**:${agent}\n正在停止 Agent Bot 的 App Server。`)], "blue");
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const blockingTaskCount = Math.max(0, view.blockingTaskCount ?? 0);
|
|
408
|
+
const releaseInSeconds = Math.max(1, view.releaseInSeconds ?? 1);
|
|
409
|
+
return sectionCard("等待释放", [
|
|
410
|
+
markdown([
|
|
411
|
+
`**Agent**:${agent}`,
|
|
412
|
+
blockingTaskCount > 0
|
|
413
|
+
? `**等待任务**:${blockingTaskCount}`
|
|
414
|
+
: `**自动释放**:${releaseInSeconds} 秒后`,
|
|
415
|
+
blockingTaskCount > 0
|
|
416
|
+
? "> 任务结束后自动释放;立即释放会中断执行。"
|
|
417
|
+
: "> 可立即释放或取消。",
|
|
418
|
+
].join("\n")),
|
|
419
|
+
{
|
|
420
|
+
tag: "column_set",
|
|
421
|
+
flex_mode: "none",
|
|
422
|
+
horizontal_spacing: "8px",
|
|
423
|
+
vertical_align: "center",
|
|
424
|
+
columns: [
|
|
425
|
+
{
|
|
426
|
+
tag: "column",
|
|
427
|
+
width: "auto",
|
|
428
|
+
vertical_align: "center",
|
|
429
|
+
elements: [{
|
|
430
|
+
tag: "button",
|
|
431
|
+
text: { tag: "plain_text", content: "Release Now" },
|
|
432
|
+
type: "danger",
|
|
433
|
+
size: "small",
|
|
434
|
+
behaviors: [{
|
|
435
|
+
type: "callback",
|
|
436
|
+
value: {
|
|
437
|
+
action: "app_server_release_now",
|
|
438
|
+
contextKey: view.contextKey,
|
|
439
|
+
scheduleId: view.scheduleId,
|
|
440
|
+
agentName: view.agentName,
|
|
441
|
+
},
|
|
442
|
+
}],
|
|
443
|
+
}],
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
tag: "column",
|
|
447
|
+
width: "auto",
|
|
448
|
+
vertical_align: "center",
|
|
449
|
+
elements: [{
|
|
450
|
+
tag: "button",
|
|
451
|
+
text: { tag: "plain_text", content: "Cancel" },
|
|
452
|
+
type: "default",
|
|
453
|
+
size: "small",
|
|
454
|
+
behaviors: [{
|
|
455
|
+
type: "callback",
|
|
456
|
+
value: {
|
|
457
|
+
action: "app_server_release_cancel",
|
|
458
|
+
contextKey: view.contextKey,
|
|
459
|
+
scheduleId: view.scheduleId,
|
|
460
|
+
agentName: view.agentName,
|
|
461
|
+
},
|
|
462
|
+
}],
|
|
463
|
+
}],
|
|
464
|
+
},
|
|
465
|
+
],
|
|
466
|
+
},
|
|
467
|
+
], "orange");
|
|
468
|
+
}
|
|
469
|
+
|
|
374
470
|
renderShellCommandCard(view: ShellCommandCardView): Record<string, unknown> {
|
|
375
471
|
const rawOutput = normalizeTerminalOutput(view.output).trimEnd();
|
|
376
472
|
const displayOutput = truncateMiddle(
|
|
@@ -273,8 +273,12 @@ function renderPost(
|
|
|
273
273
|
for (const rawElement of row) {
|
|
274
274
|
if (!isRecord(rawElement)) continue;
|
|
275
275
|
const tag = textValue(rawElement.tag);
|
|
276
|
-
if (tag === "text"
|
|
276
|
+
if (tag === "text") {
|
|
277
277
|
rowText += textValue(rawElement.text);
|
|
278
|
+
} else if (tag === "a") {
|
|
279
|
+
const text = textValue(rawElement.text);
|
|
280
|
+
const href = textValue(rawElement.href);
|
|
281
|
+
rowText += href && href !== text ? `${text || href} (${href})` : text || href;
|
|
278
282
|
} else if (tag === "at") {
|
|
279
283
|
rowText += renderMention(rawElement, mentions);
|
|
280
284
|
} else if (tag === "img") {
|
package/src/index.ts
CHANGED
|
@@ -432,6 +432,8 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
|
|
|
432
432
|
return { ok: true, data: await controller.controlGetTaskStatus(request.localSessionId) };
|
|
433
433
|
case "task_stop":
|
|
434
434
|
return { ok: true, message: await controller.controlStopTask(request.localSessionId) };
|
|
435
|
+
case "task_release":
|
|
436
|
+
return { ok: true, data: await controller.controlReleaseTask(request.localSessionId) };
|
|
435
437
|
case "task_archive":
|
|
436
438
|
return { ok: true, data: await controller.controlArchiveTask(request.localSessionId) };
|
|
437
439
|
case "task_dismiss":
|