@keyou007/agent-bot 0.1.10 → 0.1.12
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/.env.example +1 -1
- package/CHANGELOG.md +25 -0
- package/README.md +55 -43
- package/README.zh.md +55 -43
- package/assets/agent-bot-logo.png +0 -0
- package/config.example.yaml +11 -1
- package/dist/acp/AcpSessionManager.js +78 -46
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/cli/FeishuAppConfiguration.js +1 -1
- package/dist/cli/FeishuAppConfiguration.js.map +1 -1
- package/dist/cli/FeishuAppRegistration.js +2 -2
- package/dist/cli/FeishuAppRegistration.js.map +1 -1
- package/dist/cli/Initializer.js +1 -1
- package/dist/cli/Initializer.js.map +1 -1
- package/dist/cli/ServerStarter.js +7 -2
- package/dist/cli/ServerStarter.js.map +1 -1
- package/dist/cli/controlProtocol.js.map +1 -1
- package/dist/cli/help.js +20 -16
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/taskGroupOptions.js +95 -0
- package/dist/cli/taskGroupOptions.js.map +1 -0
- package/dist/cli.js +74 -2
- package/dist/cli.js.map +1 -1
- package/dist/codex/AppServerConnection.js +15 -1
- package/dist/codex/AppServerConnection.js.map +1 -1
- package/dist/codex/CodexLocalActivityDetector.js +4 -1
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +1 -0
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +112 -11
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/commands/CommandRouter.js +64 -49
- package/dist/commands/CommandRouter.js.map +1 -1
- package/dist/config/loadConfig.js +11 -1
- package/dist/config/loadConfig.js.map +1 -1
- package/dist/config/schema.js +6 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/console/ConsoleConnector.js +1 -1
- package/dist/console/ConsoleConnector.js.map +1 -1
- package/dist/console/ConsoleTurnPresenter.js +15 -8
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/deprecated-cli.js +5 -0
- package/dist/deprecated-cli.js.map +1 -0
- package/dist/feishu/CardRenderer.js +350 -15
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuMessageClient.js +51 -5
- package/dist/feishu/FeishuMessageClient.js.map +1 -1
- package/dist/feishu/FeishuTurnPresenter.js +17 -8
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/feishu/transport.js +1 -1
- package/dist/feishu/transport.js.map +1 -1
- package/dist/index.js +38 -19
- package/dist/index.js.map +1 -1
- package/dist/presentation/OutboundRouter.js +2 -2
- package/dist/presentation/OutboundRouter.js.map +1 -1
- package/dist/presentation/TurnStateReducer.js +2 -1
- package/dist/presentation/TurnStateReducer.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +665 -225
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/AcpRuntimeAdapter.js +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/runtime/AgentRuntimeRegistry.js +7 -4
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -1
- package/dist/runtime/createAgentRuntimeRegistry.js +22 -0
- package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +2 -2
- package/dist/startup/SessionMetadataHydrator.js.map +1 -1
- package/dist/startup/StartupNotifier.js +1 -0
- package/dist/startup/StartupNotifier.js.map +1 -1
- package/dist/state/StateStore.js +21 -15
- package/dist/state/StateStore.js.map +1 -1
- package/dist/supervision/SafeRestartScheduler.js +5 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -1
- package/dist/supervision/systemEnvironment.js +129 -0
- package/dist/supervision/systemEnvironment.js.map +1 -0
- package/dist/supervisor.js +13 -1
- package/dist/supervisor.js.map +1 -1
- package/docs/technical-reference.md +35 -21
- package/docs/technical-reference.zh.md +35 -21
- package/npm-shrinkwrap.json +4 -3
- package/package.json +4 -2
- package/skills/agent-bot/SKILL.md +73 -30
- package/src/acp/AcpSessionManager.ts +73 -54
- package/src/cli/FeishuAppConfiguration.ts +2 -2
- package/src/cli/FeishuAppRegistration.ts +4 -4
- package/src/cli/Initializer.ts +2 -2
- package/src/cli/ServerStarter.ts +8 -3
- package/src/cli/controlProtocol.ts +21 -1
- package/src/cli/help.ts +20 -16
- package/src/cli/taskGroupOptions.ts +149 -0
- package/src/cli.ts +98 -4
- package/src/codex/AppServerConnection.ts +21 -1
- package/src/codex/CodexLocalActivityDetector.ts +8 -1
- package/src/codex/CodexProcessManager.ts +1 -0
- package/src/codex/CodexRuntime.ts +137 -15
- package/src/commands/CommandRouter.ts +71 -49
- package/src/commands/commandTypes.ts +6 -11
- package/src/config/loadConfig.ts +11 -1
- package/src/config/schema.ts +7 -1
- package/src/console/ConsoleConnector.ts +1 -1
- package/src/console/ConsoleTurnPresenter.ts +21 -8
- package/src/deprecated-cli.ts +9 -0
- package/src/feishu/CardRenderer.ts +423 -15
- package/src/feishu/FeishuMessageClient.ts +66 -3
- package/src/feishu/FeishuTurnPresenter.ts +22 -3
- package/src/feishu/transport.ts +1 -1
- package/src/index.ts +54 -24
- package/src/presentation/OutboundRouter.ts +15 -3
- package/src/presentation/TurnStateReducer.ts +2 -0
- package/src/presentation/turnViewTypes.ts +1 -0
- package/src/proxy/ProxySessionController.ts +870 -228
- package/src/runtime/AcpRuntimeAdapter.ts +1 -0
- package/src/runtime/AgentRuntimeRegistry.ts +7 -6
- package/src/runtime/createAgentRuntimeRegistry.ts +25 -0
- package/src/runtime/types.ts +18 -0
- package/src/startup/SessionMetadataHydrator.ts +2 -2
- package/src/startup/StartupNotifier.ts +1 -0
- package/src/state/StateStore.ts +29 -15
- package/src/supervision/SafeRestartScheduler.ts +5 -0
- package/src/supervision/systemEnvironment.ts +174 -0
- package/src/supervisor.ts +12 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
|
|
2
|
+
|
|
3
|
+
export interface TaskNewGroupOptions {
|
|
4
|
+
reference: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
cwd?: string;
|
|
7
|
+
agentName?: string;
|
|
8
|
+
projectless: boolean;
|
|
9
|
+
json: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface TaskForkGroupOptions {
|
|
13
|
+
reference: string;
|
|
14
|
+
title?: string;
|
|
15
|
+
json: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseTaskNewGroupOptions(
|
|
19
|
+
input: string[],
|
|
20
|
+
language: CliLanguage = cliLanguage,
|
|
21
|
+
): TaskNewGroupOptions {
|
|
22
|
+
const positionals: string[] = [];
|
|
23
|
+
let cwd: string | undefined;
|
|
24
|
+
let agentName: string | undefined;
|
|
25
|
+
let projectless = false;
|
|
26
|
+
let json = false;
|
|
27
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
28
|
+
const argument = input[index]!;
|
|
29
|
+
if (argument === "--json") {
|
|
30
|
+
json = true;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (argument === "--nodir") {
|
|
34
|
+
if (projectless) throw optionError("newgroup", "--nodir", language);
|
|
35
|
+
if (cwd !== undefined) throw conflictingProjectOptions(language);
|
|
36
|
+
projectless = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (argument === "--dir") {
|
|
40
|
+
if (projectless) throw conflictingProjectOptions(language);
|
|
41
|
+
if (cwd !== undefined) throw optionError("newgroup", "--dir", language);
|
|
42
|
+
const directory = input[index + 1];
|
|
43
|
+
if (!directory || directory.startsWith("--")) {
|
|
44
|
+
throw new Error(cliText(
|
|
45
|
+
"task newgroup requires a project directory after --dir.",
|
|
46
|
+
"task newgroup 需要在 --dir 后指定项目目录。",
|
|
47
|
+
language,
|
|
48
|
+
));
|
|
49
|
+
}
|
|
50
|
+
cwd = directory;
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (argument === "--agent") {
|
|
55
|
+
if (agentName !== undefined) throw optionError("newgroup", "--agent", language);
|
|
56
|
+
const name = input[index + 1];
|
|
57
|
+
if (!name?.trim() || name.startsWith("--")) {
|
|
58
|
+
throw new Error(cliText(
|
|
59
|
+
"task newgroup requires an Agent standard name after --agent.",
|
|
60
|
+
"task newgroup 需要在 --agent 后指定 Agent 标准名。",
|
|
61
|
+
language,
|
|
62
|
+
));
|
|
63
|
+
}
|
|
64
|
+
agentName = name.trim();
|
|
65
|
+
index += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (argument.startsWith("--")) throw unsupportedOption("newgroup", argument, language);
|
|
69
|
+
positionals.push(argument);
|
|
70
|
+
}
|
|
71
|
+
const [reference, ...titleParts] = positionals;
|
|
72
|
+
requireTaskReference("newgroup", reference, language);
|
|
73
|
+
return {
|
|
74
|
+
reference,
|
|
75
|
+
title: titleParts.join(" ").trim() || undefined,
|
|
76
|
+
cwd,
|
|
77
|
+
...(agentName ? { agentName } : {}),
|
|
78
|
+
projectless,
|
|
79
|
+
json,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function parseTaskForkGroupOptions(
|
|
84
|
+
input: string[],
|
|
85
|
+
language: CliLanguage = cliLanguage,
|
|
86
|
+
): TaskForkGroupOptions {
|
|
87
|
+
const positionals: string[] = [];
|
|
88
|
+
let json = false;
|
|
89
|
+
for (const argument of input) {
|
|
90
|
+
if (argument === "--json") {
|
|
91
|
+
json = true;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (argument.startsWith("--")) throw unsupportedOption("forkgroup", argument, language);
|
|
95
|
+
positionals.push(argument);
|
|
96
|
+
}
|
|
97
|
+
const [reference, ...titleParts] = positionals;
|
|
98
|
+
requireTaskReference("forkgroup", reference, language);
|
|
99
|
+
return {
|
|
100
|
+
reference,
|
|
101
|
+
title: titleParts.join(" ").trim() || undefined,
|
|
102
|
+
json,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function requireTaskReference(
|
|
107
|
+
action: "newgroup" | "forkgroup",
|
|
108
|
+
reference: string | undefined,
|
|
109
|
+
language: CliLanguage,
|
|
110
|
+
): asserts reference is string {
|
|
111
|
+
if (reference && !reference.startsWith("--")) return;
|
|
112
|
+
throw new Error(cliText(
|
|
113
|
+
`task ${action} requires a source task number or task ID.`,
|
|
114
|
+
`task ${action} 需要源任务序号或任务 ID。`,
|
|
115
|
+
language,
|
|
116
|
+
));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function optionError(
|
|
120
|
+
action: "newgroup",
|
|
121
|
+
option: "--agent" | "--dir" | "--nodir",
|
|
122
|
+
language: CliLanguage,
|
|
123
|
+
): Error {
|
|
124
|
+
return new Error(cliText(
|
|
125
|
+
`task ${action} accepts ${option} only once.`,
|
|
126
|
+
`task ${action} 只能指定一次 ${option}。`,
|
|
127
|
+
language,
|
|
128
|
+
));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function conflictingProjectOptions(language: CliLanguage): Error {
|
|
132
|
+
return new Error(cliText(
|
|
133
|
+
"task newgroup cannot combine --dir and --nodir.",
|
|
134
|
+
"task newgroup 的 --dir 和 --nodir 不能同时使用。",
|
|
135
|
+
language,
|
|
136
|
+
));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function unsupportedOption(
|
|
140
|
+
action: "newgroup" | "forkgroup",
|
|
141
|
+
option: string,
|
|
142
|
+
language: CliLanguage,
|
|
143
|
+
): Error {
|
|
144
|
+
return new Error(cliText(
|
|
145
|
+
`task ${action} does not support option: ${option}.`,
|
|
146
|
+
`task ${action} 不支持参数:${option}。`,
|
|
147
|
+
language,
|
|
148
|
+
));
|
|
149
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { sendControlRequest, isServerReachable } from "./cli/LocalControlClient.
|
|
|
7
7
|
import {
|
|
8
8
|
controlEndpoint,
|
|
9
9
|
type ControlResponse,
|
|
10
|
+
type TaskGroupControlData,
|
|
10
11
|
type TaskStatusControlData,
|
|
11
12
|
} from "./cli/controlProtocol.js";
|
|
12
13
|
import {
|
|
@@ -49,6 +50,10 @@ import {
|
|
|
49
50
|
type ServerStartResult,
|
|
50
51
|
} from "./cli/ServerStarter.js";
|
|
51
52
|
import { taskChatRoute } from "./cli/taskChatRoute.js";
|
|
53
|
+
import {
|
|
54
|
+
parseTaskForkGroupOptions,
|
|
55
|
+
parseTaskNewGroupOptions,
|
|
56
|
+
} from "./cli/taskGroupOptions.js";
|
|
52
57
|
import { StateStore, type SessionRecord } from "./state/StateStore.js";
|
|
53
58
|
import {
|
|
54
59
|
nodeDiagnosticReportArguments,
|
|
@@ -99,8 +104,8 @@ async function main(input: string[]): Promise<void> {
|
|
|
99
104
|
return;
|
|
100
105
|
}
|
|
101
106
|
throw new Error(cliText(
|
|
102
|
-
`Unknown command: ${command}. Run
|
|
103
|
-
`未知命令:${command}。运行
|
|
107
|
+
`Unknown command: ${command}. Run agentbot --help for usage.`,
|
|
108
|
+
`未知命令:${command}。运行 agentbot --help 查看用法。`,
|
|
104
109
|
));
|
|
105
110
|
}
|
|
106
111
|
|
|
@@ -246,8 +251,8 @@ async function assertResetProfileServerStopped(configPath?: string): Promise<voi
|
|
|
246
251
|
if (await isServerReachable(endpoint)) {
|
|
247
252
|
throw new Error(
|
|
248
253
|
cliText(
|
|
249
|
-
"Cannot reset a running profile. Stop it with
|
|
250
|
-
"无法重置正在运行的 Profile。请先执行
|
|
254
|
+
"Cannot reset a running profile. Stop it with agentbot --profile <directory> server stop, then try again.",
|
|
255
|
+
"无法重置正在运行的 Profile。请先执行 agentbot --profile <目录> server stop,然后重试。",
|
|
251
256
|
),
|
|
252
257
|
);
|
|
253
258
|
}
|
|
@@ -410,6 +415,51 @@ async function taskCommand(input: string[]): Promise<void> {
|
|
|
410
415
|
process.stdout.write(cliText("Prompt submitted.\n", "提示词已提交。\n"));
|
|
411
416
|
return;
|
|
412
417
|
}
|
|
418
|
+
if (action === "newgroup") {
|
|
419
|
+
const options = parseTaskNewGroupOptions(rest);
|
|
420
|
+
const session = resolveTask(allSessions, options.reference);
|
|
421
|
+
const targetAgentName = options.agentName ?? session.agentName;
|
|
422
|
+
const targetAgent = config.agents[targetAgentName];
|
|
423
|
+
if (!targetAgent) {
|
|
424
|
+
throw new Error(cliText(
|
|
425
|
+
`Unknown Agent standard name: ${targetAgentName}.`,
|
|
426
|
+
`未知的 Agent 标准名:${targetAgentName}。`,
|
|
427
|
+
));
|
|
428
|
+
}
|
|
429
|
+
if (options.projectless && targetAgent.kind !== "app-server") {
|
|
430
|
+
throw new Error(cliText(
|
|
431
|
+
"task newgroup --nodir is only available for App Server agents.",
|
|
432
|
+
"task newgroup --nodir 仅适用于 App Server Agent。",
|
|
433
|
+
));
|
|
434
|
+
}
|
|
435
|
+
requireCliGroupUser(config.feishu.userOpenId);
|
|
436
|
+
const response = await sendControlRequest(controlEndpoint(config.storage.sqlitePath), {
|
|
437
|
+
action: "task_new_group",
|
|
438
|
+
localSessionId: session.localSessionId,
|
|
439
|
+
...(options.title ? { title: options.title } : {}),
|
|
440
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
441
|
+
...(options.agentName ? { agentName: options.agentName } : {}),
|
|
442
|
+
...(options.projectless ? { projectless: true } : {}),
|
|
443
|
+
}, 120_000);
|
|
444
|
+
const result = taskGroupControlData(response);
|
|
445
|
+
if (options.json) printJson(result);
|
|
446
|
+
else printTaskGroupResult(result, "newgroup");
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (action === "forkgroup") {
|
|
450
|
+
const options = parseTaskForkGroupOptions(rest);
|
|
451
|
+
const session = resolveTask(allSessions, options.reference);
|
|
452
|
+
requireCliGroupUser(config.feishu.userOpenId);
|
|
453
|
+
const response = await sendControlRequest(controlEndpoint(config.storage.sqlitePath), {
|
|
454
|
+
action: "task_fork_group",
|
|
455
|
+
localSessionId: session.localSessionId,
|
|
456
|
+
...(options.title ? { title: options.title } : {}),
|
|
457
|
+
}, 120_000);
|
|
458
|
+
const result = taskGroupControlData(response);
|
|
459
|
+
if (options.json) printJson(result);
|
|
460
|
+
else printTaskGroupResult(result, "forkgroup");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
413
463
|
const sessions = filterSessions(allSessions, rest);
|
|
414
464
|
if (action === "list") {
|
|
415
465
|
if (rest.includes("--json")) printJson(sessions);
|
|
@@ -840,6 +890,50 @@ function ensureOk(response: ControlResponse): void {
|
|
|
840
890
|
));
|
|
841
891
|
}
|
|
842
892
|
|
|
893
|
+
function requireCliGroupUser(userOpenId: string | undefined): asserts userOpenId is string {
|
|
894
|
+
if (userOpenId?.startsWith("ou_")) return;
|
|
895
|
+
throw new Error(cliText(
|
|
896
|
+
"The profile has no authorizing Lark user to invite. Run agentbot init again or configure FEISHU_USER_OPEN_ID.",
|
|
897
|
+
"当前 Profile 没有可邀请的飞书授权用户。请重新运行 agentbot init,或配置 FEISHU_USER_OPEN_ID。",
|
|
898
|
+
));
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function taskGroupControlData(response: ControlResponse): TaskGroupControlData {
|
|
902
|
+
ensureOk(response);
|
|
903
|
+
const data = response.data as Partial<TaskGroupControlData> | undefined;
|
|
904
|
+
if (
|
|
905
|
+
!data
|
|
906
|
+
|| typeof data.sourceLocalSessionId !== "string"
|
|
907
|
+
|| !data.group
|
|
908
|
+
|| typeof data.group.chatId !== "string"
|
|
909
|
+
|| typeof data.group.contextKey !== "string"
|
|
910
|
+
|| typeof data.group.name !== "string"
|
|
911
|
+
|| !data.task
|
|
912
|
+
|| typeof data.task.localSessionId !== "string"
|
|
913
|
+
) {
|
|
914
|
+
throw new Error(cliText(
|
|
915
|
+
"Agent Bot returned an invalid task-group result.",
|
|
916
|
+
"Agent Bot 返回了无效的任务群结果。",
|
|
917
|
+
));
|
|
918
|
+
}
|
|
919
|
+
return data as TaskGroupControlData;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function printTaskGroupResult(
|
|
923
|
+
result: TaskGroupControlData,
|
|
924
|
+
action: "newgroup" | "forkgroup",
|
|
925
|
+
): void {
|
|
926
|
+
process.stdout.write(cliText(
|
|
927
|
+
`${action === "forkgroup" ? "Fork group" : "Lark group"} created: ${result.group.name}\n`,
|
|
928
|
+
`${action === "forkgroup" ? "Fork 群" : "飞书群"}已创建:${result.group.name}\n`,
|
|
929
|
+
));
|
|
930
|
+
process.stdout.write(`${cliText("Chat ID: ", "群 ID:")}${result.group.chatId}\n`);
|
|
931
|
+
process.stdout.write(`${cliText("Task ID: ", "任务 ID:")}${result.task.localSessionId}\n`);
|
|
932
|
+
if (result.task.remoteSessionId) {
|
|
933
|
+
process.stdout.write(`${cliText("Agent task ID: ", "Agent 任务 ID:")}${result.task.remoteSessionId}\n`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
843
937
|
function optionValue(args: string[], name: string): string | undefined {
|
|
844
938
|
const index = args.indexOf(name);
|
|
845
939
|
return index >= 0 ? args[index + 1] : undefined;
|
|
@@ -26,6 +26,21 @@ interface PendingRequest {
|
|
|
26
26
|
|
|
27
27
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
28
28
|
|
|
29
|
+
export class AppServerRequestError extends Error {
|
|
30
|
+
readonly data: unknown;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
readonly method: string,
|
|
34
|
+
readonly code: number,
|
|
35
|
+
readonly serverMessage: string,
|
|
36
|
+
data?: unknown,
|
|
37
|
+
) {
|
|
38
|
+
super(`${method} failed: ${serverMessage}`);
|
|
39
|
+
this.name = "AppServerRequestError";
|
|
40
|
+
this.data = data;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
export class AppServerConnection {
|
|
30
45
|
private nextId = 1;
|
|
31
46
|
private closed = false;
|
|
@@ -131,7 +146,12 @@ export class AppServerConnection {
|
|
|
131
146
|
this.pending.delete(message.id);
|
|
132
147
|
if (pending.timeout) clearTimeout(pending.timeout);
|
|
133
148
|
if ("error" in message) {
|
|
134
|
-
pending.reject(new
|
|
149
|
+
pending.reject(new AppServerRequestError(
|
|
150
|
+
pending.method,
|
|
151
|
+
message.error.code,
|
|
152
|
+
message.error.message,
|
|
153
|
+
message.error.data,
|
|
154
|
+
));
|
|
135
155
|
} else {
|
|
136
156
|
pending.resolve(message.result);
|
|
137
157
|
}
|
|
@@ -16,12 +16,14 @@ interface ThreadPathRow {
|
|
|
16
16
|
|
|
17
17
|
interface ThreadSettingsRow {
|
|
18
18
|
id: string;
|
|
19
|
+
model_provider?: string | null;
|
|
19
20
|
model: string | null;
|
|
20
21
|
reasoning_effort: string | null;
|
|
21
22
|
approval_mode: string | null;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
export interface CodexLocalThreadSettings {
|
|
26
|
+
modelProvider?: string;
|
|
25
27
|
model?: string;
|
|
26
28
|
reasoningEffort?: string;
|
|
27
29
|
permissionMode?: PermissionMode;
|
|
@@ -79,10 +81,15 @@ export class CodexLocalActivityDetector {
|
|
|
79
81
|
fileMustExist: true,
|
|
80
82
|
});
|
|
81
83
|
const placeholders = ids.map(() => "?").join(", ");
|
|
84
|
+
const columns = new Set(
|
|
85
|
+
(database.pragma("table_info(threads)") as Array<{ name: string }>).map((column) => column.name),
|
|
86
|
+
);
|
|
87
|
+
const providerColumn = columns.has("model_provider") ? ", model_provider" : "";
|
|
82
88
|
const rows = database.prepare(
|
|
83
|
-
`SELECT id, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
|
|
89
|
+
`SELECT id${providerColumn}, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
|
|
84
90
|
).all(...ids) as ThreadSettingsRow[];
|
|
85
91
|
return new Map(rows.map((row) => [row.id, {
|
|
92
|
+
...(row.model_provider?.trim() ? { modelProvider: row.model_provider.trim() } : {}),
|
|
86
93
|
...(row.model?.trim() ? { model: row.model.trim() } : {}),
|
|
87
94
|
...(row.reasoning_effort?.trim() ? { reasoningEffort: row.reasoning_effort.trim() } : {}),
|
|
88
95
|
...(row.approval_mode?.trim()
|
|
@@ -40,6 +40,7 @@ export class CodexProcessManager implements AppServerClientProvider {
|
|
|
40
40
|
});
|
|
41
41
|
await client.request("initialize", {
|
|
42
42
|
clientInfo: { name: "agent-bot", title: "Agent Bot", version: "0.1.0" },
|
|
43
|
+
capabilities: { experimentalApi: true },
|
|
43
44
|
});
|
|
44
45
|
client.notify("initialized", {});
|
|
45
46
|
return client;
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
CreateRuntimeSessionInput,
|
|
6
6
|
ForkRuntimeSessionInput,
|
|
7
7
|
ModelOption,
|
|
8
|
+
ModelProviderOption,
|
|
8
9
|
PermissionMode,
|
|
9
10
|
RemoteSessionActivity,
|
|
10
11
|
RemoteSessionPage,
|
|
@@ -13,12 +14,14 @@ import type {
|
|
|
13
14
|
RuntimeGoal,
|
|
14
15
|
RuntimeGoalUpdate,
|
|
15
16
|
RuntimeEvent,
|
|
17
|
+
RuntimeExecutionSettings,
|
|
16
18
|
RuntimePrompt,
|
|
17
19
|
RuntimeSession,
|
|
18
20
|
RuntimeSessionMetadata,
|
|
19
21
|
} from "../runtime/types.js";
|
|
20
22
|
import { appendGeneratedImageMarkdown } from "../utils/generatedImageMarkdown.js";
|
|
21
23
|
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
24
|
+
import { AppServerRequestError } from "./AppServerConnection.js";
|
|
22
25
|
import { mapCodexNotification } from "./CodexEventMapper.js";
|
|
23
26
|
import { CodexLocalActivityDetector } from "./CodexLocalActivityDetector.js";
|
|
24
27
|
import { detectProjectlessWorkspace } from "./ProjectlessWorkspace.js";
|
|
@@ -32,12 +35,13 @@ const WINDOWS_SCREENSHOT_DEVELOPER_INSTRUCTIONS = [
|
|
|
32
35
|
"Call Graphics.CopyFromScreen with those physical coordinates and validate that the saved bitmap dimensions exactly equal the selected physical capture bounds.",
|
|
33
36
|
].join(" ");
|
|
34
37
|
|
|
35
|
-
const SESSION_REQUEST_TIMEOUT_MS =
|
|
38
|
+
const SESSION_REQUEST_TIMEOUT_MS = 60_000;
|
|
36
39
|
const CONTROL_REQUEST_TIMEOUT_MS = 10_000;
|
|
37
40
|
const SYNC_REQUEST_TIMEOUT_MS = 5_000;
|
|
38
41
|
// A timed-out fork keeps running in App Server and can create an orphan thread.
|
|
39
42
|
// Wait for its response; connection closure still rejects the request.
|
|
40
43
|
const FORK_REQUEST_TIMEOUT_MS = 0;
|
|
44
|
+
type ThreadListSortKey = "recency_at" | "updated_at";
|
|
41
45
|
|
|
42
46
|
export interface AppServerClient {
|
|
43
47
|
request<T = unknown>(method: string, params?: unknown, timeoutMs?: number): Promise<T>;
|
|
@@ -81,6 +85,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
81
85
|
private readonly unsubscribeDisconnect?: () => void;
|
|
82
86
|
private readonly sessionSyncs = new Map<string, Promise<RuntimeSession>>();
|
|
83
87
|
private readonly localActivityDetector?: CodexLocalActivityDetector;
|
|
88
|
+
private threadListSortKey: ThreadListSortKey = "recency_at";
|
|
84
89
|
|
|
85
90
|
constructor(
|
|
86
91
|
private readonly provider: AppServerClientProvider,
|
|
@@ -100,6 +105,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
100
105
|
const response = await client.request<ThreadResponse>("thread/start", {
|
|
101
106
|
cwd: input.cwd,
|
|
102
107
|
model: input.model,
|
|
108
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
103
109
|
threadSource: "user",
|
|
104
110
|
...threadLifecycleParams(input.cwd),
|
|
105
111
|
...permissionParams(input.permissionMode),
|
|
@@ -122,8 +128,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
122
128
|
const client = await this.client();
|
|
123
129
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
124
130
|
threadId: input.remoteSessionId,
|
|
131
|
+
excludeTurns: true,
|
|
125
132
|
cwd: input.cwd,
|
|
126
133
|
model: input.model,
|
|
134
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
127
135
|
...threadLifecycleParams(input.cwd),
|
|
128
136
|
...permissionParams(input.permissionMode),
|
|
129
137
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
@@ -135,15 +143,30 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
135
143
|
|
|
136
144
|
async forkSession(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
|
|
137
145
|
const client = await this.client();
|
|
138
|
-
const
|
|
146
|
+
const forkParams = {
|
|
139
147
|
threadId: input.remoteSessionId,
|
|
140
148
|
lastTurnId: input.lastTurnId,
|
|
141
149
|
cwd: input.cwd,
|
|
142
150
|
model: input.model,
|
|
151
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
143
152
|
threadSource: "user",
|
|
144
153
|
...threadLifecycleParams(input.cwd),
|
|
145
154
|
...permissionParams(input.permissionMode),
|
|
146
|
-
}
|
|
155
|
+
};
|
|
156
|
+
let response: ThreadResponse;
|
|
157
|
+
try {
|
|
158
|
+
response = await client.request<ThreadResponse>("thread/fork", {
|
|
159
|
+
...forkParams,
|
|
160
|
+
excludeTurns: true,
|
|
161
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (!isUnsupportedExcludeTurnsError(error)) throw error;
|
|
164
|
+
this.logger.warn(
|
|
165
|
+
{ error },
|
|
166
|
+
"Codex App Server does not support thread/fork excludeTurns; retrying without it.",
|
|
167
|
+
);
|
|
168
|
+
response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
|
|
169
|
+
}
|
|
147
170
|
const requestedTitle = normalizeTaskTitle(input.title);
|
|
148
171
|
if (requestedTitle) {
|
|
149
172
|
await client.request("thread/name/set", {
|
|
@@ -194,18 +217,18 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
194
217
|
limit?: number;
|
|
195
218
|
} = {}): Promise<RemoteSessionPage> {
|
|
196
219
|
const client = await this.client();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
220
|
+
let response: ThreadListResponse;
|
|
221
|
+
try {
|
|
222
|
+
response = await this.requestThreadList(client, input, this.threadListSortKey);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (this.threadListSortKey !== "recency_at" || !isUnsupportedRecencySortError(error)) throw error;
|
|
225
|
+
this.threadListSortKey = "updated_at";
|
|
226
|
+
this.logger.warn(
|
|
227
|
+
{ error },
|
|
228
|
+
"App Server does not support thread/list recency_at sorting; retrying with updated_at.",
|
|
229
|
+
);
|
|
230
|
+
response = await this.requestThreadList(client, input, this.threadListSortKey);
|
|
231
|
+
}
|
|
209
232
|
const sessions = await Promise.all(response.data.map(async (thread) => {
|
|
210
233
|
const listed = remoteSessionSummary(thread);
|
|
211
234
|
if (listed.status === "active" || listed.lastTurnStatus === "inProgress") return listed;
|
|
@@ -229,6 +252,25 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
229
252
|
};
|
|
230
253
|
}
|
|
231
254
|
|
|
255
|
+
private requestThreadList(
|
|
256
|
+
client: AppServerClient,
|
|
257
|
+
input: { searchTerm?: string; cursor?: string; limit?: number },
|
|
258
|
+
sortKey: ThreadListSortKey,
|
|
259
|
+
): Promise<ThreadListResponse> {
|
|
260
|
+
return client.request<ThreadListResponse>(
|
|
261
|
+
"thread/list",
|
|
262
|
+
{
|
|
263
|
+
cursor: input.cursor,
|
|
264
|
+
limit: input.limit ?? 20,
|
|
265
|
+
sortKey,
|
|
266
|
+
sortDirection: "desc",
|
|
267
|
+
archived: false,
|
|
268
|
+
searchTerm: input.searchTerm,
|
|
269
|
+
},
|
|
270
|
+
CONTROL_REQUEST_TIMEOUT_MS,
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
232
274
|
async readRemoteSession(remoteSessionId: string): Promise<RemoteSessionSummary> {
|
|
233
275
|
const response = await (await this.client()).request<ThreadReadResponse>(
|
|
234
276
|
"thread/read",
|
|
@@ -375,6 +417,28 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
375
417
|
this.requireSession(sessionId).permissionMode = mode;
|
|
376
418
|
}
|
|
377
419
|
|
|
420
|
+
async setExecutionSettings(
|
|
421
|
+
sessionId: string,
|
|
422
|
+
settings: RuntimeExecutionSettings,
|
|
423
|
+
): Promise<RuntimeSession> {
|
|
424
|
+
const session = this.requireSession(sessionId);
|
|
425
|
+
const response = await (await this.client()).request<ThreadResponse>("thread/resume", {
|
|
426
|
+
threadId: session.remoteSessionId,
|
|
427
|
+
excludeTurns: true,
|
|
428
|
+
cwd: session.cwd,
|
|
429
|
+
modelProvider: settings.modelProvider,
|
|
430
|
+
model: settings.model,
|
|
431
|
+
...threadLifecycleParams(session.cwd),
|
|
432
|
+
...permissionParams(settings.permissionMode),
|
|
433
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
434
|
+
session.modelProvider = response.modelProvider ?? settings.modelProvider;
|
|
435
|
+
session.model = response.model ?? settings.model;
|
|
436
|
+
session.reasoningEffort = settings.reasoningEffort;
|
|
437
|
+
session.permissionMode = settings.permissionMode;
|
|
438
|
+
session.needsResume = false;
|
|
439
|
+
return session;
|
|
440
|
+
}
|
|
441
|
+
|
|
378
442
|
async respondToApproval(
|
|
379
443
|
sessionId: string,
|
|
380
444
|
requestId: string,
|
|
@@ -412,6 +476,33 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
412
476
|
}));
|
|
413
477
|
}
|
|
414
478
|
|
|
479
|
+
async listModelProviders(): Promise<ModelProviderOption[]> {
|
|
480
|
+
const response = await (await this.client()).request<{ config?: Record<string, unknown> }>(
|
|
481
|
+
"config/read",
|
|
482
|
+
{},
|
|
483
|
+
CONTROL_REQUEST_TIMEOUT_MS,
|
|
484
|
+
);
|
|
485
|
+
const config = response.config ?? {};
|
|
486
|
+
const defaultProvider = stringValue(config.model_provider)?.trim();
|
|
487
|
+
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
488
|
+
const providers = new Map<string, ModelProviderOption>();
|
|
489
|
+
for (const [id, value] of Object.entries(configuredProviders)) {
|
|
490
|
+
if (!id.trim()) continue;
|
|
491
|
+
const displayName = isRecord(value) ? stringValue(value.name)?.trim() : undefined;
|
|
492
|
+
providers.set(id, {
|
|
493
|
+
id,
|
|
494
|
+
...(displayName ? { displayName } : {}),
|
|
495
|
+
...(id === defaultProvider ? { isDefault: true } : {}),
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
if (defaultProvider && !providers.has(defaultProvider)) {
|
|
499
|
+
providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
|
|
500
|
+
}
|
|
501
|
+
return [...providers.values()].sort((left, right) =>
|
|
502
|
+
Number(Boolean(right.isDefault)) - Number(Boolean(left.isDefault)) || left.id.localeCompare(right.id),
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
415
506
|
onEvent(listener: (event: RuntimeEvent) => void): () => void {
|
|
416
507
|
this.listeners.add(listener);
|
|
417
508
|
return () => this.listeners.delete(listener);
|
|
@@ -642,8 +733,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
642
733
|
if (!session.needsResume) return;
|
|
643
734
|
await client.request("thread/resume", {
|
|
644
735
|
threadId: session.remoteSessionId,
|
|
736
|
+
excludeTurns: true,
|
|
645
737
|
cwd: session.cwd,
|
|
646
738
|
model: session.model,
|
|
739
|
+
...(session.modelProvider ? { modelProvider: session.modelProvider } : {}),
|
|
647
740
|
...threadLifecycleParams(session.cwd),
|
|
648
741
|
...permissionParams(session.permissionMode),
|
|
649
742
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
@@ -772,6 +865,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
772
865
|
title: normalizeTaskTitle(response.thread.name)
|
|
773
866
|
?? normalizeTaskTitle(response.thread.preview)
|
|
774
867
|
?? input.title,
|
|
868
|
+
modelProvider: input.modelProvider ?? response.modelProvider,
|
|
775
869
|
model: input.model ?? response.model,
|
|
776
870
|
reasoningEffort,
|
|
777
871
|
permissionMode: input.permissionMode,
|
|
@@ -835,6 +929,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
835
929
|
|
|
836
930
|
interface ThreadResponse {
|
|
837
931
|
thread: { id: string; name?: string | null; preview?: string };
|
|
932
|
+
modelProvider?: string;
|
|
838
933
|
model?: string;
|
|
839
934
|
reasoningEffort?: string | null;
|
|
840
935
|
}
|
|
@@ -853,6 +948,7 @@ interface CodexThreadSnapshot {
|
|
|
853
948
|
name?: string | null;
|
|
854
949
|
preview?: string;
|
|
855
950
|
cwd?: string;
|
|
951
|
+
modelProvider?: string;
|
|
856
952
|
source?: unknown;
|
|
857
953
|
createdAt?: number;
|
|
858
954
|
updatedAt?: number;
|
|
@@ -933,6 +1029,31 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
933
1029
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
934
1030
|
}
|
|
935
1031
|
|
|
1032
|
+
function isUnsupportedExcludeTurnsError(error: unknown): boolean {
|
|
1033
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/fork") return false;
|
|
1034
|
+
const data = typeof error.data === "string"
|
|
1035
|
+
? error.data
|
|
1036
|
+
: error.data === undefined
|
|
1037
|
+
? ""
|
|
1038
|
+
: JSON.stringify(error.data);
|
|
1039
|
+
const details = `${error.serverMessage} ${data}`;
|
|
1040
|
+
if (!details.toLowerCase().replaceAll(/[^a-z]/g, "").includes("excludeturns")) return false;
|
|
1041
|
+
return error.code === -32602
|
|
1042
|
+
|| /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function isUnsupportedRecencySortError(error: unknown): boolean {
|
|
1046
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/list") return false;
|
|
1047
|
+
const data = typeof error.data === "string"
|
|
1048
|
+
? error.data
|
|
1049
|
+
: error.data === undefined
|
|
1050
|
+
? ""
|
|
1051
|
+
: JSON.stringify(error.data);
|
|
1052
|
+
const details = `${error.serverMessage} ${data}`;
|
|
1053
|
+
return /recency_at/i.test(details)
|
|
1054
|
+
&& /unknown|unrecognized|unexpected|unsupported|not supported|invalid/i.test(details);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
936
1057
|
function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
|
|
937
1058
|
const lastTurn = thread.turns?.at(-1);
|
|
938
1059
|
const lastCompletedTurn = [...(thread.turns ?? [])]
|
|
@@ -956,6 +1077,7 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
|
|
|
956
1077
|
cwd: thread.cwd ?? "",
|
|
957
1078
|
source: codexSourceLabel(thread.source),
|
|
958
1079
|
status,
|
|
1080
|
+
modelProvider: thread.modelProvider,
|
|
959
1081
|
createdAt: thread.createdAt,
|
|
960
1082
|
updatedAt: thread.updatedAt,
|
|
961
1083
|
recencyAt: thread.recencyAt ?? undefined,
|