@keyou007/agent-bot 0.1.10 → 0.1.11
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 +15 -0
- package/README.md +16 -10
- package/README.zh.md +16 -10
- package/assets/agent-bot-logo.png +0 -0
- package/dist/acp/AcpSessionManager.js +78 -46
- package/dist/acp/AcpSessionManager.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 +77 -2
- 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/console/ConsoleTurnPresenter.js +1 -1
- package/dist/console/ConsoleTurnPresenter.js.map +1 -1
- package/dist/feishu/CardRenderer.js +345 -11
- 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 +8 -3
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -1
- package/dist/index.js +18 -17
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +589 -205
- 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/docs/technical-reference.md +15 -5
- package/docs/technical-reference.zh.md +15 -5
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/skills/agent-bot/SKILL.md +13 -3
- package/src/acp/AcpSessionManager.ts +73 -54
- 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 +91 -2
- package/src/commands/CommandRouter.ts +71 -49
- package/src/commands/commandTypes.ts +6 -11
- package/src/console/ConsoleTurnPresenter.ts +1 -1
- package/src/feishu/CardRenderer.ts +418 -11
- package/src/feishu/FeishuMessageClient.ts +66 -3
- package/src/feishu/FeishuTurnPresenter.ts +7 -2
- package/src/index.ts +21 -22
- package/src/proxy/ProxySessionController.ts +706 -208
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keyou007/agent-bot",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Using Codex in Lark/Feishu feels just like using native Codex.",
|
|
6
6
|
"license": "GPL-3.0-only",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
".env.example",
|
|
28
|
+
"assets/",
|
|
28
29
|
"CHANGELOG.md",
|
|
29
30
|
"config.example.yaml",
|
|
30
31
|
"dist/",
|
|
@@ -99,6 +99,8 @@ agent-bot server stop
|
|
|
99
99
|
|
|
100
100
|
Treat `server restart` as the normal restart path. It schedules a safe restart and waits for active tasks, pending final-message delivery, and a quiet inbound-message window. The first safe-restart status card is delayed by three seconds so the active task's final response can usually be delivered first; this presentation delay does not postpone scheduler polling. While the restart is pending, its status card has a bottom `Cancel` button that conditionally cancels that exact schedule and updates the card in place.
|
|
101
101
|
|
|
102
|
+
In Feishu, `/restart` schedules the same safe restart. Use `/restart --force` only when the user explicitly accepts interruption; the command accepts no other arguments.
|
|
103
|
+
|
|
102
104
|
Use `agent-bot server restart --immediate` only when the user explicitly requests an immediate restart or accepts interruption. Never use `taskkill`, `Stop-Process`, or equivalent commands for routine Agent Bot restart management.
|
|
103
105
|
|
|
104
106
|
`agent-bot console` opens the console UI only when the service is stopped. Do not use `--force` while the service is live unless the user explicitly accepts concurrent state access.
|
|
@@ -135,7 +137,11 @@ Use `/queue <prompt>` in Feishu to persist a separate follow-up Prompt instead o
|
|
|
135
137
|
|
|
136
138
|
For a long-running objective in the active Feishu task, use `/goal <objective>`. Use `/goal` to inspect it, `/goal pause` or `/goal resume` to control automatic continuation, `/goal edit <objective>` to revise it, and `/goal clear` to remove it. Stopping a Goal turn through Agent Bot also pauses the Goal before sending Interrupt so it does not immediately continue.
|
|
137
139
|
|
|
138
|
-
Use `/
|
|
140
|
+
Use `/agent` in Feishu to open the unified execution-settings card on its Agent tab and `/agent <name>` to directly select the default agent. The Agent tab is present only when multiple agents are configured and remains usable before the chat has a current task. When only one Agent is configured, `/agent` reports that current Agent directly instead of sending a card. Agent selection affects future tasks and does not replace the Agent of an existing task; use `/new` after selecting when a new task is needed.
|
|
141
|
+
|
|
142
|
+
Use `/newgroup [title] [--dir <cwd> | --nodir]` in Feishu to create a private group named `[agent] [project directory] <title>`, invite the command sender, and immediately create and bind a new task in that group. `/new` and `/newgroup` share the same project options: an explicit `--dir` overrides the inherited project directory, while `--nodir` forces a Projectless Codex task; reject using both together. Resolve `~`, `~/...`, and `~\...` from the current user's home directory for both commands. Projectless groups omit the project segment and use `[agent] <title>`. The new task inherits the source chat's current task project directory when neither option is present, plus its model, reasoning effort, and permission mode without interrupting or switching the source task. When the source chat has no current task, use the selected agent and its runtime defaults. An explicit title becomes both the group suffix and the task title. When the title is omitted, use task title `新任务` and group name `[agent] [project directory] 新任务 (mm-dd)`, or `[agent] 新任务 (mm-dd)` for Projectless; an explicit title never gets a date suffix. Every new group gets a deterministic scheme-C Identicon avatar: Latin project names display their first word, Chinese names display up to their first four characters, and the normalized full project path hashes into the palette and symmetric node pattern. Projectless groups use the title as the stable seed. Replace the user's home-directory prefix with `~`. The bracketed project-directory value is limited to 15 characters. Longer paths keep the final two directories when they fit, otherwise keep the final directory, truncating the tail only when the final directory itself is overlong. Use `\` on Windows and `/` on macOS/Linux. Do not automatically send a Sessions or Status card to the new group. A later `/new --dir <cwd>` or `/new --nodir` in the group may explicitly replace the inherited project shape.
|
|
143
|
+
|
|
144
|
+
Provider is part of the same inherited execution-settings group as model, reasoning effort, and permission mode. Preserve all four across `/new`, `/newgroup`, `/fork`, `/forkgroup`, and the matching `/sessions` actions. New tasks with no inherited Provider must leave it unset so Codex App Server uses the default from Codex configuration. New-group welcome messages report all four settings.
|
|
139
145
|
|
|
140
146
|
Group names created by `/newgroup` and `/forkgroup` are capped at 60 displayed characters. When the generated name would exceed that limit, truncate only the title portion in the Feishu group name; preserve the underlying Agent Bot task title.
|
|
141
147
|
|
|
@@ -145,11 +151,15 @@ Use `/new [title] --nodir` to force a new Codex Projectless task even when the c
|
|
|
145
151
|
|
|
146
152
|
Every task in the Feishu `/sessions` card has `New`, `NewGroup`, `Fork`, and `ForkGroup` actions. `New` inherits the source task's project directory, model, reasoning effort, and permission mode, creates a new task, and switches the current chat to it without stopping or forking the source task. `NewGroup` applies the same inheritance in a newly created private group. `Fork` switches the current chat to a branch, while `ForkGroup` binds the branch to a newly created private group; both fork actions use the selected task's latest available completed turn without interrupting an active turn.
|
|
147
153
|
|
|
148
|
-
Use `/fork [number-or-task-id]` in Feishu to branch from the current or specified Codex task and switch to the new branch. If the source task is running, Agent Bot forks from its latest completed turn and leaves the active turn running; it rejects only when the task has no completed turn yet.
|
|
154
|
+
Use `/fork [number-or-task-id]` in Feishu to branch from the current or specified Codex task and switch to the new branch. If the source task is running, Agent Bot forks from its latest completed turn and leaves the active turn running; it rejects only when the task has no completed turn yet. Every fork request sends `excludeTurns: true` to Codex App Server so the response omits populated turns without removing turns from the forked history. Older App Servers that explicitly reject this experimental field are retried once without it; ambiguous failures such as timeouts or disconnects are not retried.
|
|
149
155
|
|
|
150
156
|
Status cards include a `Refresh` action that reads the latest task state and updates the same card in place without switching tasks or sending another card.
|
|
151
157
|
|
|
152
|
-
Use `/model` in Feishu to
|
|
158
|
+
Use `/agent`, `/provider`, `/model`, `/thinking`, or `/permissions` in Feishu to open the same interactive Card 2.0 execution-settings card with the corresponding tab active. The card exposes Provider, Model, Thinking, and Permission tabs whenever a current task exists, plus an Agent tab when multiple agents are configured. Each non-current option has an English `Switch` action. Selecting an option refreshes the same card in place. Agent changes affect future tasks; the other settings apply from the next request. Model changes automatically choose a compatible reasoning effort when necessary.
|
|
159
|
+
|
|
160
|
+
If the current runtime has no alternative Provider, `/provider` reports the current Provider directly instead of opening the settings card. Apply the same rule to `/agent` when only one Agent is configured.
|
|
161
|
+
|
|
162
|
+
These four settings commands do not accept arguments. Never instruct a user to send a Provider, model, reasoning effort, or permission value after the command; direct them to the card callbacks instead.
|
|
153
163
|
|
|
154
164
|
Keep action labels placed to the right of card content or at the bottom of a card in English. Buttons above the main card content, such as activity-history navigation, may remain in Chinese.
|
|
155
165
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import type { Logger } from "pino";
|
|
3
|
-
import type {
|
|
3
|
+
import type { AgentConfig } from "../config/schema.js";
|
|
4
4
|
import type {
|
|
5
5
|
AcpInitializeResult,
|
|
6
6
|
AcpPermissionRequestParams,
|
|
@@ -13,7 +13,6 @@ import { AcpProcessManager, type ManagedAcpProcess } from "./AcpProcessManager.j
|
|
|
13
13
|
|
|
14
14
|
export interface RuntimeSession {
|
|
15
15
|
localSessionId: string;
|
|
16
|
-
processKey: string;
|
|
17
16
|
agentName: string;
|
|
18
17
|
cwd: string;
|
|
19
18
|
acpSessionId: string;
|
|
@@ -22,6 +21,8 @@ export interface RuntimeSession {
|
|
|
22
21
|
modes?: JsonValue;
|
|
23
22
|
configOptions?: JsonValue;
|
|
24
23
|
availableCommands?: JsonValue;
|
|
24
|
+
onUpdate: (session: RuntimeSession, update: Record<string, JsonValue>) => void;
|
|
25
|
+
onPermissionRequest: (session: RuntimeSession, params: AcpPermissionRequestParams) => Promise<JsonValue>;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface CreateRuntimeSessionInput {
|
|
@@ -38,9 +39,11 @@ export interface CreateRuntimeSessionInput {
|
|
|
38
39
|
export class AcpSessionManager {
|
|
39
40
|
private readonly sessions = new Map<string, RuntimeSession>();
|
|
40
41
|
private readonly sessionsByAcpId = new Map<string, RuntimeSession>();
|
|
42
|
+
private processStart?: Promise<ManagedAcpProcess>;
|
|
41
43
|
|
|
42
44
|
constructor(
|
|
43
|
-
private readonly
|
|
45
|
+
private readonly agentName: string,
|
|
46
|
+
private readonly agent: AgentConfig,
|
|
44
47
|
private readonly processManager: AcpProcessManager,
|
|
45
48
|
private readonly logger: Logger,
|
|
46
49
|
) {}
|
|
@@ -50,66 +53,19 @@ export class AcpSessionManager {
|
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
async create(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
53
|
-
|
|
54
|
-
if (!agent) {
|
|
55
|
-
throw new Error(`Unknown agent: ${input.agentName}`);
|
|
56
|
-
}
|
|
56
|
+
if (input.agentName !== this.agentName) throw new Error(`Unexpected agent: ${input.agentName}`);
|
|
57
57
|
|
|
58
58
|
const cwd = path.resolve(input.cwd);
|
|
59
|
-
const
|
|
60
|
-
const managed = this.processManager.start(processKey, input.agentName, agent);
|
|
59
|
+
const managed = await this.process();
|
|
61
60
|
const connection = managed.connection;
|
|
62
61
|
|
|
63
|
-
let runtime: RuntimeSession | undefined;
|
|
64
|
-
|
|
65
|
-
connection.registerHandler("session/request_permission", async (params) => {
|
|
66
|
-
if (!runtime) {
|
|
67
|
-
throw new Error("Received permission request before session was ready.");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
return input.onPermissionRequest(runtime, params as unknown as AcpPermissionRequestParams);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
connection.on("notification", (_method, params) => {
|
|
74
|
-
if (_method !== "session/update") {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const updateParams = params as unknown as AcpSessionUpdateParams;
|
|
79
|
-
const target = this.sessionsByAcpId.get(updateParams.sessionId);
|
|
80
|
-
if (!target) {
|
|
81
|
-
this.logger.debug({ updateParams }, "Ignoring update for unknown ACP session.");
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
this.applySessionUpdate(target, updateParams.update);
|
|
86
|
-
input.onUpdate(target, updateParams.update);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const initializeResult = await connection.request<AcpInitializeResult>("initialize", {
|
|
90
|
-
protocolVersion: 1,
|
|
91
|
-
clientCapabilities: {},
|
|
92
|
-
clientInfo: {
|
|
93
|
-
name: "agent-bot",
|
|
94
|
-
title: "Agent Bot",
|
|
95
|
-
version: "0.1.0",
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (initializeResult.authMethods?.length) {
|
|
100
|
-
const method = initializeResult.authMethods[0];
|
|
101
|
-
this.logger.info({ methodId: method.id }, "Authenticating ACP agent with first advertised method.");
|
|
102
|
-
await connection.request("authenticate", { methodId: method.id });
|
|
103
|
-
}
|
|
104
|
-
|
|
105
62
|
const newSessionResult = await connection.request<AcpSessionNewResult>("session/new", {
|
|
106
63
|
cwd,
|
|
107
64
|
mcpServers: [],
|
|
108
65
|
});
|
|
109
66
|
|
|
110
|
-
runtime = {
|
|
67
|
+
const runtime: RuntimeSession = {
|
|
111
68
|
localSessionId: input.localSessionId,
|
|
112
|
-
processKey,
|
|
113
69
|
agentName: input.agentName,
|
|
114
70
|
cwd,
|
|
115
71
|
acpSessionId: newSessionResult.sessionId,
|
|
@@ -117,6 +73,8 @@ export class AcpSessionManager {
|
|
|
117
73
|
running: false,
|
|
118
74
|
modes: newSessionResult.modes,
|
|
119
75
|
configOptions: newSessionResult.configOptions,
|
|
76
|
+
onUpdate: input.onUpdate,
|
|
77
|
+
onPermissionRequest: input.onPermissionRequest,
|
|
120
78
|
};
|
|
121
79
|
|
|
122
80
|
this.sessions.set(input.localSessionId, runtime);
|
|
@@ -169,7 +127,13 @@ export class AcpSessionManager {
|
|
|
169
127
|
|
|
170
128
|
this.sessions.delete(localSessionId);
|
|
171
129
|
this.sessionsByAcpId.delete(session.acpSessionId);
|
|
172
|
-
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
shutdown(): void {
|
|
133
|
+
this.sessions.clear();
|
|
134
|
+
this.sessionsByAcpId.clear();
|
|
135
|
+
this.processStart = undefined;
|
|
136
|
+
this.processManager.stopAll();
|
|
173
137
|
}
|
|
174
138
|
|
|
175
139
|
async setMode(localSessionId: string, modeId: string): Promise<void> {
|
|
@@ -203,6 +167,61 @@ export class AcpSessionManager {
|
|
|
203
167
|
return session;
|
|
204
168
|
}
|
|
205
169
|
|
|
170
|
+
private async process(): Promise<ManagedAcpProcess> {
|
|
171
|
+
const existing = this.processManager.get(this.agentName);
|
|
172
|
+
if (existing) return existing;
|
|
173
|
+
if (this.processStart) return this.processStart;
|
|
174
|
+
const start = this.startProcess();
|
|
175
|
+
this.processStart = start;
|
|
176
|
+
try {
|
|
177
|
+
return await start;
|
|
178
|
+
} finally {
|
|
179
|
+
if (this.processStart === start) this.processStart = undefined;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private async startProcess(): Promise<ManagedAcpProcess> {
|
|
184
|
+
const managed = this.processManager.start(this.agentName, this.agentName, this.agent);
|
|
185
|
+
try {
|
|
186
|
+
const connection = managed.connection;
|
|
187
|
+
connection.registerHandler("session/request_permission", async (params) => {
|
|
188
|
+
const permission = params as unknown as AcpPermissionRequestParams;
|
|
189
|
+
const target = this.sessionsByAcpId.get(permission.sessionId);
|
|
190
|
+
if (!target) throw new Error(`Unknown ACP session requesting permission: ${permission.sessionId}`);
|
|
191
|
+
return target.onPermissionRequest(target, permission);
|
|
192
|
+
});
|
|
193
|
+
connection.on("notification", (method, params) => {
|
|
194
|
+
if (method !== "session/update") return;
|
|
195
|
+
const updateParams = params as unknown as AcpSessionUpdateParams;
|
|
196
|
+
const target = this.sessionsByAcpId.get(updateParams.sessionId);
|
|
197
|
+
if (!target) {
|
|
198
|
+
this.logger.debug({ updateParams }, "Ignoring update for unknown ACP session.");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
this.applySessionUpdate(target, updateParams.update);
|
|
202
|
+
target.onUpdate(target, updateParams.update);
|
|
203
|
+
});
|
|
204
|
+
const initializeResult = await connection.request<AcpInitializeResult>("initialize", {
|
|
205
|
+
protocolVersion: 1,
|
|
206
|
+
clientCapabilities: {},
|
|
207
|
+
clientInfo: {
|
|
208
|
+
name: "agent-bot",
|
|
209
|
+
title: "Agent Bot",
|
|
210
|
+
version: "0.1.0",
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
if (initializeResult.authMethods?.length) {
|
|
214
|
+
const method = initializeResult.authMethods[0];
|
|
215
|
+
this.logger.info({ methodId: method.id }, "Authenticating ACP agent with first advertised method.");
|
|
216
|
+
await connection.request("authenticate", { methodId: method.id });
|
|
217
|
+
}
|
|
218
|
+
return managed;
|
|
219
|
+
} catch (error) {
|
|
220
|
+
this.processManager.stop(this.agentName);
|
|
221
|
+
throw error;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
206
225
|
private applySessionUpdate(session: RuntimeSession, update: Record<string, JsonValue>): void {
|
|
207
226
|
const updateType = update.sessionUpdate;
|
|
208
227
|
if (updateType === "config_option_update" && "configOptions" in update) {
|
|
@@ -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";
|
|
@@ -100,6 +103,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
100
103
|
const response = await client.request<ThreadResponse>("thread/start", {
|
|
101
104
|
cwd: input.cwd,
|
|
102
105
|
model: input.model,
|
|
106
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
103
107
|
threadSource: "user",
|
|
104
108
|
...threadLifecycleParams(input.cwd),
|
|
105
109
|
...permissionParams(input.permissionMode),
|
|
@@ -122,8 +126,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
122
126
|
const client = await this.client();
|
|
123
127
|
const response = await client.request<ThreadResponse>("thread/resume", {
|
|
124
128
|
threadId: input.remoteSessionId,
|
|
129
|
+
excludeTurns: true,
|
|
125
130
|
cwd: input.cwd,
|
|
126
131
|
model: input.model,
|
|
132
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
127
133
|
...threadLifecycleParams(input.cwd),
|
|
128
134
|
...permissionParams(input.permissionMode),
|
|
129
135
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
@@ -135,15 +141,30 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
135
141
|
|
|
136
142
|
async forkSession(input: ForkRuntimeSessionInput): Promise<RuntimeSession> {
|
|
137
143
|
const client = await this.client();
|
|
138
|
-
const
|
|
144
|
+
const forkParams = {
|
|
139
145
|
threadId: input.remoteSessionId,
|
|
140
146
|
lastTurnId: input.lastTurnId,
|
|
141
147
|
cwd: input.cwd,
|
|
142
148
|
model: input.model,
|
|
149
|
+
...(input.modelProvider ? { modelProvider: input.modelProvider } : {}),
|
|
143
150
|
threadSource: "user",
|
|
144
151
|
...threadLifecycleParams(input.cwd),
|
|
145
152
|
...permissionParams(input.permissionMode),
|
|
146
|
-
}
|
|
153
|
+
};
|
|
154
|
+
let response: ThreadResponse;
|
|
155
|
+
try {
|
|
156
|
+
response = await client.request<ThreadResponse>("thread/fork", {
|
|
157
|
+
...forkParams,
|
|
158
|
+
excludeTurns: true,
|
|
159
|
+
}, FORK_REQUEST_TIMEOUT_MS);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (!isUnsupportedExcludeTurnsError(error)) throw error;
|
|
162
|
+
this.logger.warn(
|
|
163
|
+
{ error },
|
|
164
|
+
"Codex App Server does not support thread/fork excludeTurns; retrying without it.",
|
|
165
|
+
);
|
|
166
|
+
response = await client.request<ThreadResponse>("thread/fork", forkParams, FORK_REQUEST_TIMEOUT_MS);
|
|
167
|
+
}
|
|
147
168
|
const requestedTitle = normalizeTaskTitle(input.title);
|
|
148
169
|
if (requestedTitle) {
|
|
149
170
|
await client.request("thread/name/set", {
|
|
@@ -375,6 +396,28 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
375
396
|
this.requireSession(sessionId).permissionMode = mode;
|
|
376
397
|
}
|
|
377
398
|
|
|
399
|
+
async setExecutionSettings(
|
|
400
|
+
sessionId: string,
|
|
401
|
+
settings: RuntimeExecutionSettings,
|
|
402
|
+
): Promise<RuntimeSession> {
|
|
403
|
+
const session = this.requireSession(sessionId);
|
|
404
|
+
const response = await (await this.client()).request<ThreadResponse>("thread/resume", {
|
|
405
|
+
threadId: session.remoteSessionId,
|
|
406
|
+
excludeTurns: true,
|
|
407
|
+
cwd: session.cwd,
|
|
408
|
+
modelProvider: settings.modelProvider,
|
|
409
|
+
model: settings.model,
|
|
410
|
+
...threadLifecycleParams(session.cwd),
|
|
411
|
+
...permissionParams(settings.permissionMode),
|
|
412
|
+
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
413
|
+
session.modelProvider = response.modelProvider ?? settings.modelProvider;
|
|
414
|
+
session.model = response.model ?? settings.model;
|
|
415
|
+
session.reasoningEffort = settings.reasoningEffort;
|
|
416
|
+
session.permissionMode = settings.permissionMode;
|
|
417
|
+
session.needsResume = false;
|
|
418
|
+
return session;
|
|
419
|
+
}
|
|
420
|
+
|
|
378
421
|
async respondToApproval(
|
|
379
422
|
sessionId: string,
|
|
380
423
|
requestId: string,
|
|
@@ -412,6 +455,33 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
412
455
|
}));
|
|
413
456
|
}
|
|
414
457
|
|
|
458
|
+
async listModelProviders(): Promise<ModelProviderOption[]> {
|
|
459
|
+
const response = await (await this.client()).request<{ config?: Record<string, unknown> }>(
|
|
460
|
+
"config/read",
|
|
461
|
+
{},
|
|
462
|
+
CONTROL_REQUEST_TIMEOUT_MS,
|
|
463
|
+
);
|
|
464
|
+
const config = response.config ?? {};
|
|
465
|
+
const defaultProvider = stringValue(config.model_provider)?.trim();
|
|
466
|
+
const configuredProviders = isRecord(config.model_providers) ? config.model_providers : {};
|
|
467
|
+
const providers = new Map<string, ModelProviderOption>();
|
|
468
|
+
for (const [id, value] of Object.entries(configuredProviders)) {
|
|
469
|
+
if (!id.trim()) continue;
|
|
470
|
+
const displayName = isRecord(value) ? stringValue(value.name)?.trim() : undefined;
|
|
471
|
+
providers.set(id, {
|
|
472
|
+
id,
|
|
473
|
+
...(displayName ? { displayName } : {}),
|
|
474
|
+
...(id === defaultProvider ? { isDefault: true } : {}),
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
if (defaultProvider && !providers.has(defaultProvider)) {
|
|
478
|
+
providers.set(defaultProvider, { id: defaultProvider, isDefault: true });
|
|
479
|
+
}
|
|
480
|
+
return [...providers.values()].sort((left, right) =>
|
|
481
|
+
Number(Boolean(right.isDefault)) - Number(Boolean(left.isDefault)) || left.id.localeCompare(right.id),
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
415
485
|
onEvent(listener: (event: RuntimeEvent) => void): () => void {
|
|
416
486
|
this.listeners.add(listener);
|
|
417
487
|
return () => this.listeners.delete(listener);
|
|
@@ -642,8 +712,10 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
642
712
|
if (!session.needsResume) return;
|
|
643
713
|
await client.request("thread/resume", {
|
|
644
714
|
threadId: session.remoteSessionId,
|
|
715
|
+
excludeTurns: true,
|
|
645
716
|
cwd: session.cwd,
|
|
646
717
|
model: session.model,
|
|
718
|
+
...(session.modelProvider ? { modelProvider: session.modelProvider } : {}),
|
|
647
719
|
...threadLifecycleParams(session.cwd),
|
|
648
720
|
...permissionParams(session.permissionMode),
|
|
649
721
|
}, SESSION_REQUEST_TIMEOUT_MS);
|
|
@@ -772,6 +844,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
772
844
|
title: normalizeTaskTitle(response.thread.name)
|
|
773
845
|
?? normalizeTaskTitle(response.thread.preview)
|
|
774
846
|
?? input.title,
|
|
847
|
+
modelProvider: input.modelProvider ?? response.modelProvider,
|
|
775
848
|
model: input.model ?? response.model,
|
|
776
849
|
reasoningEffort,
|
|
777
850
|
permissionMode: input.permissionMode,
|
|
@@ -835,6 +908,7 @@ export class CodexRuntime implements AgentRuntime {
|
|
|
835
908
|
|
|
836
909
|
interface ThreadResponse {
|
|
837
910
|
thread: { id: string; name?: string | null; preview?: string };
|
|
911
|
+
modelProvider?: string;
|
|
838
912
|
model?: string;
|
|
839
913
|
reasoningEffort?: string | null;
|
|
840
914
|
}
|
|
@@ -853,6 +927,7 @@ interface CodexThreadSnapshot {
|
|
|
853
927
|
name?: string | null;
|
|
854
928
|
preview?: string;
|
|
855
929
|
cwd?: string;
|
|
930
|
+
modelProvider?: string;
|
|
856
931
|
source?: unknown;
|
|
857
932
|
createdAt?: number;
|
|
858
933
|
updatedAt?: number;
|
|
@@ -933,6 +1008,19 @@ function uniqueStrings(values: string[]): string[] {
|
|
|
933
1008
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
934
1009
|
}
|
|
935
1010
|
|
|
1011
|
+
function isUnsupportedExcludeTurnsError(error: unknown): boolean {
|
|
1012
|
+
if (!(error instanceof AppServerRequestError) || error.method !== "thread/fork") return false;
|
|
1013
|
+
const data = typeof error.data === "string"
|
|
1014
|
+
? error.data
|
|
1015
|
+
: error.data === undefined
|
|
1016
|
+
? ""
|
|
1017
|
+
: JSON.stringify(error.data);
|
|
1018
|
+
const details = `${error.serverMessage} ${data}`;
|
|
1019
|
+
if (!details.toLowerCase().replaceAll(/[^a-z]/g, "").includes("excludeturns")) return false;
|
|
1020
|
+
return error.code === -32602
|
|
1021
|
+
|| /unknown|unrecognized|unexpected|unsupported|not supported|experimental/i.test(details);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
936
1024
|
function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary {
|
|
937
1025
|
const lastTurn = thread.turns?.at(-1);
|
|
938
1026
|
const lastCompletedTurn = [...(thread.turns ?? [])]
|
|
@@ -956,6 +1044,7 @@ function remoteSessionSummary(thread: CodexThreadSnapshot): RemoteSessionSummary
|
|
|
956
1044
|
cwd: thread.cwd ?? "",
|
|
957
1045
|
source: codexSourceLabel(thread.source),
|
|
958
1046
|
status,
|
|
1047
|
+
modelProvider: thread.modelProvider,
|
|
959
1048
|
createdAt: thread.createdAt,
|
|
960
1049
|
updatedAt: thread.updatedAt,
|
|
961
1050
|
recencyAt: thread.recencyAt ?? undefined,
|