@keyou007/agent-bot 0.1.0
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 +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +675 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/SECURITY.md +13 -0
- package/config.example.yaml +56 -0
- package/dist/acp/AcpJsonRpcConnection.js +155 -0
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
- package/dist/acp/AcpProcessManager.js +65 -0
- package/dist/acp/AcpProcessManager.js.map +1 -0
- package/dist/acp/AcpSessionManager.js +164 -0
- package/dist/acp/AcpSessionManager.js.map +1 -0
- package/dist/acp/acpTypes.js +10 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +349 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -0
- package/dist/cli/FeishuAppRegistration.js +160 -0
- package/dist/cli/FeishuAppRegistration.js.map +1 -0
- package/dist/cli/Initializer.js +112 -0
- package/dist/cli/Initializer.js.map +1 -0
- package/dist/cli/LocalControlClient.js +47 -0
- package/dist/cli/LocalControlClient.js.map +1 -0
- package/dist/cli/LocalControlServer.js +66 -0
- package/dist/cli/LocalControlServer.js.map +1 -0
- package/dist/cli/SkillRegistry.js +126 -0
- package/dist/cli/SkillRegistry.js.map +1 -0
- package/dist/cli/controlProtocol.js +10 -0
- package/dist/cli/controlProtocol.js.map +1 -0
- package/dist/cli/taskChatRoute.js +23 -0
- package/dist/cli/taskChatRoute.js.map +1 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/AppServerConnection.js +131 -0
- package/dist/codex/AppServerConnection.js.map +1 -0
- package/dist/codex/CodexEventMapper.js +296 -0
- package/dist/codex/CodexEventMapper.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +124 -0
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
- package/dist/codex/CodexProcessManager.js +64 -0
- package/dist/codex/CodexProcessManager.js.map +1 -0
- package/dist/codex/CodexRuntime.js +825 -0
- package/dist/codex/CodexRuntime.js.map +1 -0
- package/dist/codex/ProjectlessWorkspace.js +74 -0
- package/dist/codex/ProjectlessWorkspace.js.map +1 -0
- package/dist/codex/protocol.js +10 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/commands/CommandRouter.js +176 -0
- package/dist/commands/CommandRouter.js.map +1 -0
- package/dist/commands/commandTypes.js +2 -0
- package/dist/commands/commandTypes.js.map +1 -0
- package/dist/config/loadConfig.js +107 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/paths.js +32 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.js +38 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/console/ConsoleConnector.js +39 -0
- package/dist/console/ConsoleConnector.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +60 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -0
- package/dist/feishu/CardRenderer.js +972 -0
- package/dist/feishu/CardRenderer.js.map +1 -0
- package/dist/feishu/CardUpdateScheduler.js +148 -0
- package/dist/feishu/CardUpdateScheduler.js.map +1 -0
- package/dist/feishu/ConsoleFeishuClient.js +18 -0
- package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +257 -0
- package/dist/feishu/FeishuConnector.js.map +1 -0
- package/dist/feishu/FeishuMarkdown.js +37 -0
- package/dist/feishu/FeishuMarkdown.js.map +1 -0
- package/dist/feishu/FeishuMessageClient.js +459 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -0
- package/dist/feishu/FeishuTurnPresenter.js +425 -0
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
- package/dist/feishu/GroupAvatarGenerator.js +86 -0
- package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
- package/dist/feishu/LocalCardImage.js +13 -0
- package/dist/feishu/LocalCardImage.js.map +1 -0
- package/dist/feishu/LocalImageMarkdown.js +89 -0
- package/dist/feishu/LocalImageMarkdown.js.map +1 -0
- package/dist/feishu/contextKey.js +12 -0
- package/dist/feishu/contextKey.js.map +1 -0
- package/dist/feishu/transport.js +14 -0
- package/dist/feishu/transport.js.map +1 -0
- package/dist/feishu/types.js +2 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +22 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +127 -0
- package/dist/presentation/OutboundRouter.js.map +1 -0
- package/dist/presentation/TurnStateReducer.js +307 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -0
- package/dist/presentation/splitMarkdown.js +341 -0
- package/dist/presentation/splitMarkdown.js.map +1 -0
- package/dist/presentation/turnViewTypes.js +2 -0
- package/dist/presentation/turnViewTypes.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +2819 -0
- package/dist/proxy/ProxySessionController.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +271 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
- package/dist/runtime/AgentRuntimeRegistry.js +18 -0
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
- package/dist/runtime/agentEnvironment.js +11 -0
- package/dist/runtime/agentEnvironment.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +46 -0
- package/dist/startup/SessionMetadataHydrator.js.map +1 -0
- package/dist/startup/StartupNotifier.js +78 -0
- package/dist/startup/StartupNotifier.js.map +1 -0
- package/dist/startup/startFeishu.js +6 -0
- package/dist/startup/startFeishu.js.map +1 -0
- package/dist/state/StateStore.js +893 -0
- package/dist/state/StateStore.js.map +1 -0
- package/dist/state/migrations.js +129 -0
- package/dist/state/migrations.js.map +1 -0
- package/dist/supervision/SafeRestartNotifier.js +116 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -0
- package/dist/supervision/SafeRestartScheduler.js +123 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -0
- package/dist/supervision/replacementSupervisor.js +8 -0
- package/dist/supervision/replacementSupervisor.js.map +1 -0
- package/dist/supervision/restartPolicy.js +16 -0
- package/dist/supervision/restartPolicy.js.map +1 -0
- package/dist/supervisor.js +94 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/utils/executeShellCommand.js +76 -0
- package/dist/utils/executeShellCommand.js.map +1 -0
- package/dist/utils/id.js +5 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/markdown.js +29 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/spawnCommand.js +21 -0
- package/dist/utils/spawnCommand.js.map +1 -0
- package/dist/utils/taskTitle.js +10 -0
- package/dist/utils/taskTitle.js.map +1 -0
- package/docs/technical-reference.md +294 -0
- package/docs/technical-reference.zh.md +294 -0
- package/npm-shrinkwrap.json +4525 -0
- package/package.json +87 -0
- package/skills/agent-bot/SKILL.md +141 -0
- package/skills/agent-bot/agents/openai.yaml +4 -0
- package/src/acp/AcpJsonRpcConnection.ts +212 -0
- package/src/acp/AcpProcessManager.ts +87 -0
- package/src/acp/AcpSessionManager.ts +227 -0
- package/src/acp/acpTypes.ts +99 -0
- package/src/cli/FeishuAppConfiguration.ts +473 -0
- package/src/cli/FeishuAppRegistration.ts +207 -0
- package/src/cli/Initializer.ts +159 -0
- package/src/cli/LocalControlClient.ts +48 -0
- package/src/cli/LocalControlServer.ts +67 -0
- package/src/cli/SkillRegistry.ts +151 -0
- package/src/cli/controlProtocol.ts +34 -0
- package/src/cli/taskChatRoute.ts +31 -0
- package/src/cli.ts +630 -0
- package/src/codex/AppServerConnection.ts +159 -0
- package/src/codex/CodexEventMapper.ts +351 -0
- package/src/codex/CodexLocalActivityDetector.ts +148 -0
- package/src/codex/CodexProcessManager.ts +63 -0
- package/src/codex/CodexRuntime.ts +1025 -0
- package/src/codex/ProjectlessWorkspace.ts +90 -0
- package/src/codex/protocol.ts +37 -0
- package/src/commands/CommandRouter.ts +179 -0
- package/src/commands/commandTypes.ts +28 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/paths.ts +37 -0
- package/src/config/schema.ts +46 -0
- package/src/console/ConsoleConnector.ts +40 -0
- package/src/console/ConsoleTurnPresenter.ts +64 -0
- package/src/feishu/CardRenderer.ts +1157 -0
- package/src/feishu/CardUpdateScheduler.ts +165 -0
- package/src/feishu/ConsoleFeishuClient.ts +22 -0
- package/src/feishu/FeishuConnector.ts +277 -0
- package/src/feishu/FeishuMarkdown.ts +47 -0
- package/src/feishu/FeishuMessageClient.ts +649 -0
- package/src/feishu/FeishuTurnPresenter.ts +539 -0
- package/src/feishu/GroupAvatarGenerator.ts +102 -0
- package/src/feishu/LocalCardImage.ts +13 -0
- package/src/feishu/LocalImageMarkdown.ts +92 -0
- package/src/feishu/contextKey.ts +14 -0
- package/src/feishu/transport.ts +24 -0
- package/src/feishu/types.ts +83 -0
- package/src/index.ts +228 -0
- package/src/logging/logger.ts +28 -0
- package/src/presentation/OutboundRouter.ts +205 -0
- package/src/presentation/TurnStateReducer.ts +341 -0
- package/src/presentation/splitMarkdown.ts +358 -0
- package/src/presentation/turnViewTypes.ts +54 -0
- package/src/proxy/ProxySessionController.ts +3311 -0
- package/src/runtime/AcpRuntimeAdapter.ts +324 -0
- package/src/runtime/AgentRuntimeRegistry.ts +20 -0
- package/src/runtime/agentEnvironment.ts +14 -0
- package/src/runtime/types.ts +215 -0
- package/src/startup/SessionMetadataHydrator.ts +46 -0
- package/src/startup/StartupNotifier.ts +92 -0
- package/src/startup/startFeishu.ts +19 -0
- package/src/state/StateStore.ts +1249 -0
- package/src/state/migrations.ts +128 -0
- package/src/supervision/SafeRestartNotifier.ts +122 -0
- package/src/supervision/SafeRestartScheduler.ts +149 -0
- package/src/supervision/replacementSupervisor.ts +10 -0
- package/src/supervision/restartPolicy.ts +19 -0
- package/src/supervisor.ts +106 -0
- package/src/utils/executeShellCommand.ts +97 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/spawnCommand.ts +35 -0
- package/src/utils/taskTitle.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
import type { Logger } from "pino";
|
|
4
|
+
import {
|
|
5
|
+
isNotification,
|
|
6
|
+
isRequest,
|
|
7
|
+
isResponse,
|
|
8
|
+
type AppServerMessage,
|
|
9
|
+
type AppServerRequestId,
|
|
10
|
+
type AppServerResponse,
|
|
11
|
+
} from "./protocol.js";
|
|
12
|
+
|
|
13
|
+
type ServerRequestHandler = (
|
|
14
|
+
params: unknown,
|
|
15
|
+
id: AppServerRequestId,
|
|
16
|
+
method: string,
|
|
17
|
+
) => Promise<unknown>;
|
|
18
|
+
type NotificationListener = (method: string, params: unknown) => void;
|
|
19
|
+
|
|
20
|
+
interface PendingRequest {
|
|
21
|
+
method: string;
|
|
22
|
+
resolve: (value: unknown) => void;
|
|
23
|
+
reject: (error: Error) => void;
|
|
24
|
+
timeout?: NodeJS.Timeout;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
28
|
+
|
|
29
|
+
export class AppServerConnection {
|
|
30
|
+
private nextId = 1;
|
|
31
|
+
private closed = false;
|
|
32
|
+
private readonly pending = new Map<AppServerRequestId, PendingRequest>();
|
|
33
|
+
private readonly requestHandlers = new Map<string, ServerRequestHandler>();
|
|
34
|
+
private readonly notificationListeners = new Set<NotificationListener>();
|
|
35
|
+
|
|
36
|
+
constructor(
|
|
37
|
+
private readonly process: ChildProcessWithoutNullStreams,
|
|
38
|
+
private readonly logger: Logger,
|
|
39
|
+
) {
|
|
40
|
+
this.bindOutput();
|
|
41
|
+
this.process.once("close", () => this.close());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
request<T = unknown>(method: string, params?: unknown, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS): Promise<T> {
|
|
45
|
+
if (this.closed) {
|
|
46
|
+
return Promise.reject(new Error(`Cannot send App Server request after connection closed: ${method}`));
|
|
47
|
+
}
|
|
48
|
+
const id = this.nextId++;
|
|
49
|
+
const promise = new Promise<unknown>((resolve, reject) => {
|
|
50
|
+
const pending: PendingRequest = { method, resolve, reject };
|
|
51
|
+
if (timeoutMs > 0) {
|
|
52
|
+
pending.timeout = setTimeout(() => {
|
|
53
|
+
this.pending.delete(id);
|
|
54
|
+
reject(new Error(`App Server request timed out: ${method}`));
|
|
55
|
+
}, timeoutMs);
|
|
56
|
+
}
|
|
57
|
+
this.pending.set(id, pending);
|
|
58
|
+
});
|
|
59
|
+
this.write({ id, method, ...(params === undefined ? {} : { params }) });
|
|
60
|
+
return promise as Promise<T>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
notify(method: string, params?: unknown): void {
|
|
64
|
+
if (this.closed) return;
|
|
65
|
+
this.write({ method, ...(params === undefined ? {} : { params }) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
registerRequestHandler(method: string, handler: ServerRequestHandler): void {
|
|
69
|
+
this.requestHandlers.set(method, handler);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onNotification(listener: NotificationListener): () => void {
|
|
73
|
+
this.notificationListeners.add(listener);
|
|
74
|
+
return () => this.notificationListeners.delete(listener);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
close(): void {
|
|
78
|
+
if (this.closed) return;
|
|
79
|
+
this.closed = true;
|
|
80
|
+
for (const request of this.pending.values()) {
|
|
81
|
+
if (request.timeout) clearTimeout(request.timeout);
|
|
82
|
+
request.reject(new Error("App Server connection closed."));
|
|
83
|
+
}
|
|
84
|
+
this.pending.clear();
|
|
85
|
+
this.notificationListeners.clear();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private bindOutput(): void {
|
|
89
|
+
const reader = readline.createInterface({ input: this.process.stdout, crlfDelay: Infinity });
|
|
90
|
+
reader.on("line", (line) => {
|
|
91
|
+
if (!line.trim()) return;
|
|
92
|
+
let message: AppServerMessage;
|
|
93
|
+
try {
|
|
94
|
+
message = JSON.parse(line) as AppServerMessage;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
this.logger.warn({ line, error }, "Ignoring non-JSON Codex App Server stdout line.");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.handleMessage(message);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private handleMessage(message: AppServerMessage): void {
|
|
104
|
+
if (isResponse(message)) {
|
|
105
|
+
this.handleResponse(message);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (isRequest(message)) {
|
|
109
|
+
void this.handleServerRequest(message.id, message.method, message.params);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (isNotification(message)) {
|
|
113
|
+
for (const listener of this.notificationListeners) listener(message.method, message.params);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.logger.warn({ message }, "Ignoring unknown Codex App Server message shape.");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private handleResponse(message: AppServerResponse): void {
|
|
120
|
+
const pending = this.pending.get(message.id);
|
|
121
|
+
if (!pending) {
|
|
122
|
+
this.logger.warn(
|
|
123
|
+
{
|
|
124
|
+
responseId: message.id,
|
|
125
|
+
responseKind: "error" in message ? "error" : "result",
|
|
126
|
+
},
|
|
127
|
+
"Received Codex App Server response for unknown request.",
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this.pending.delete(message.id);
|
|
132
|
+
if (pending.timeout) clearTimeout(pending.timeout);
|
|
133
|
+
if ("error" in message) {
|
|
134
|
+
pending.reject(new Error(`${pending.method} failed: ${message.error.message}`));
|
|
135
|
+
} else {
|
|
136
|
+
pending.resolve(message.result);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async handleServerRequest(id: AppServerRequestId, method: string, params: unknown): Promise<void> {
|
|
141
|
+
const handler = this.requestHandlers.get(method);
|
|
142
|
+
if (!handler) {
|
|
143
|
+
this.write({ id, error: { code: -32601, message: `Client method is not implemented: ${method}` } });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
this.write({ id, result: await handler(params, id, method) });
|
|
148
|
+
} catch (error) {
|
|
149
|
+
this.write({
|
|
150
|
+
id,
|
|
151
|
+
error: { code: -32000, message: error instanceof Error ? error.message : String(error) },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private write(message: AppServerMessage): void {
|
|
157
|
+
this.process.stdin.write(`${JSON.stringify(message)}\n`, "utf8");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { PlanStep, ToolState } from "../runtime/types.js";
|
|
2
|
+
|
|
3
|
+
export type MappedCodexNotification =
|
|
4
|
+
| { kind: "turn_started"; threadId: string; turnId: string; startedAt?: number }
|
|
5
|
+
| {
|
|
6
|
+
kind: "token_usage";
|
|
7
|
+
threadId: string;
|
|
8
|
+
turnId: string;
|
|
9
|
+
lastTokens: number;
|
|
10
|
+
cumulativeTokens: number;
|
|
11
|
+
}
|
|
12
|
+
| { kind: "agent_delta"; threadId: string; turnId: string; itemId: string; text: string }
|
|
13
|
+
| {
|
|
14
|
+
kind: "agent_message_phase";
|
|
15
|
+
threadId: string;
|
|
16
|
+
turnId: string;
|
|
17
|
+
itemId: string;
|
|
18
|
+
phase: "commentary" | "final_answer";
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
kind: "progress";
|
|
22
|
+
threadId: string;
|
|
23
|
+
turnId: string;
|
|
24
|
+
activityId: string;
|
|
25
|
+
text: string;
|
|
26
|
+
append: true;
|
|
27
|
+
}
|
|
28
|
+
| { kind: "plan"; threadId: string; turnId: string; steps: PlanStep[] }
|
|
29
|
+
| { kind: "tool"; phase: "started" | "updated"; threadId: string; turnId: string; tool: ToolState }
|
|
30
|
+
| { kind: "tool_output_delta"; threadId: string; turnId: string; toolId: string; delta: string }
|
|
31
|
+
| {
|
|
32
|
+
kind: "terminal";
|
|
33
|
+
threadId: string;
|
|
34
|
+
turnId: string;
|
|
35
|
+
status: "completed" | "cancelled" | "failed";
|
|
36
|
+
durationMs?: number;
|
|
37
|
+
error?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function mapCodexNotification(method: string, params: unknown): MappedCodexNotification | undefined {
|
|
41
|
+
if (!isRecord(params)) return undefined;
|
|
42
|
+
const threadId = stringValue(params.threadId);
|
|
43
|
+
const turnId = stringValue(params.turnId) ?? (isRecord(params.turn) ? stringValue(params.turn.id) : undefined);
|
|
44
|
+
if (!threadId || !turnId) return undefined;
|
|
45
|
+
|
|
46
|
+
if (method === "turn/started") {
|
|
47
|
+
return {
|
|
48
|
+
kind: "turn_started",
|
|
49
|
+
threadId,
|
|
50
|
+
turnId,
|
|
51
|
+
startedAt: isRecord(params.turn) ? secondsToMilliseconds(numberValue(params.turn.startedAt)) : undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
method === "thread/tokenUsage/updated"
|
|
57
|
+
&& isRecord(params.tokenUsage)
|
|
58
|
+
&& isRecord(params.tokenUsage.last)
|
|
59
|
+
&& isRecord(params.tokenUsage.total)
|
|
60
|
+
) {
|
|
61
|
+
const lastTokens = effectiveTokenCount(params.tokenUsage.last);
|
|
62
|
+
const cumulativeTokens = effectiveTokenCount(params.tokenUsage.total);
|
|
63
|
+
if (lastTokens === undefined || cumulativeTokens === undefined) return undefined;
|
|
64
|
+
return { kind: "token_usage", threadId, turnId, lastTokens, cumulativeTokens };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (method === "item/agentMessage/delta") {
|
|
68
|
+
const text = stringValue(params.delta);
|
|
69
|
+
const itemId = stringValue(params.itemId);
|
|
70
|
+
return text === undefined || !itemId ? undefined : { kind: "agent_delta", threadId, turnId, itemId, text };
|
|
71
|
+
}
|
|
72
|
+
if (method === "item/commandExecution/outputDelta") {
|
|
73
|
+
const delta = stringValue(params.delta);
|
|
74
|
+
const toolId = stringValue(params.itemId);
|
|
75
|
+
return delta === undefined || !toolId
|
|
76
|
+
? undefined
|
|
77
|
+
: { kind: "tool_output_delta", threadId, turnId, toolId, delta };
|
|
78
|
+
}
|
|
79
|
+
if (method === "item/reasoning/summaryTextDelta") {
|
|
80
|
+
const text = stringValue(params.delta);
|
|
81
|
+
const itemId = stringValue(params.itemId);
|
|
82
|
+
const summaryIndex = numberValue(params.summaryIndex);
|
|
83
|
+
if (text === undefined || !itemId || summaryIndex === undefined) return undefined;
|
|
84
|
+
return {
|
|
85
|
+
kind: "progress",
|
|
86
|
+
threadId,
|
|
87
|
+
turnId,
|
|
88
|
+
activityId: `reasoning:${itemId}:${summaryIndex}`,
|
|
89
|
+
text,
|
|
90
|
+
append: true,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (method === "turn/plan/updated" && Array.isArray(params.plan)) {
|
|
94
|
+
const steps = params.plan.flatMap((value): PlanStep[] => {
|
|
95
|
+
if (!isRecord(value) || typeof value.step !== "string") return [];
|
|
96
|
+
return [{ text: value.step, status: mapPlanStatus(value.status) }];
|
|
97
|
+
});
|
|
98
|
+
return { kind: "plan", threadId, turnId, steps };
|
|
99
|
+
}
|
|
100
|
+
if ((method === "item/started" || method === "item/completed") && isRecord(params.item)) {
|
|
101
|
+
if (params.item.type === "agentMessage") {
|
|
102
|
+
const itemId = stringValue(params.item.id);
|
|
103
|
+
const phase = messagePhase(params.item.phase);
|
|
104
|
+
if (!itemId || !phase) return undefined;
|
|
105
|
+
return { kind: "agent_message_phase", threadId, turnId, itemId, phase };
|
|
106
|
+
}
|
|
107
|
+
const phase = method === "item/started" ? "started" : "updated";
|
|
108
|
+
const tool = mapTool(params.item, phase, numberValue(params.startedAtMs), numberValue(params.completedAtMs));
|
|
109
|
+
if (!tool) return undefined;
|
|
110
|
+
return {
|
|
111
|
+
kind: "tool",
|
|
112
|
+
phase,
|
|
113
|
+
threadId,
|
|
114
|
+
turnId,
|
|
115
|
+
tool,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (method === "turn/completed" && isRecord(params.turn)) {
|
|
119
|
+
const status = params.turn.status;
|
|
120
|
+
const mappedStatus = status === "interrupted" ? "cancelled" : status === "failed" ? "failed" : "completed";
|
|
121
|
+
const error = isRecord(params.turn.error) ? stringValue(params.turn.error.message) : undefined;
|
|
122
|
+
return {
|
|
123
|
+
kind: "terminal",
|
|
124
|
+
threadId,
|
|
125
|
+
turnId,
|
|
126
|
+
status: mappedStatus,
|
|
127
|
+
durationMs: numberValue(params.turn.durationMs),
|
|
128
|
+
error,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mapTool(
|
|
135
|
+
item: Record<string, unknown>,
|
|
136
|
+
phase: "started" | "updated",
|
|
137
|
+
startedAt?: number,
|
|
138
|
+
completedAt?: number,
|
|
139
|
+
): ToolState | undefined {
|
|
140
|
+
const id = stringValue(item.id);
|
|
141
|
+
const type = stringValue(item.type);
|
|
142
|
+
if (!id || !type) return undefined;
|
|
143
|
+
const status = mapToolStatus(item.status, phase);
|
|
144
|
+
if (type === "commandExecution") {
|
|
145
|
+
const command = stringValue(item.command) ?? "Command";
|
|
146
|
+
return {
|
|
147
|
+
id,
|
|
148
|
+
title: command,
|
|
149
|
+
kind: "command",
|
|
150
|
+
status,
|
|
151
|
+
command,
|
|
152
|
+
output: stringValue(item.aggregatedOutput),
|
|
153
|
+
exitCode: numberValue(item.exitCode),
|
|
154
|
+
startedAt,
|
|
155
|
+
completedAt,
|
|
156
|
+
error: status === "failed" ? stringValue(item.aggregatedOutput) : undefined,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
if (type === "fileChange") {
|
|
160
|
+
const files = Array.isArray(item.changes)
|
|
161
|
+
? item.changes.flatMap((change): NonNullable<ToolState["files"]> => {
|
|
162
|
+
if (!isRecord(change) || typeof change.path !== "string") return [];
|
|
163
|
+
const diff = stringValue(change.diff) ?? "";
|
|
164
|
+
return [{ path: change.path, additions: countDiff(diff, "+"), deletions: countDiff(diff, "-") }];
|
|
165
|
+
})
|
|
166
|
+
: [];
|
|
167
|
+
return { id, title: `修改 ${files.length} 个文件`, kind: "file_change", status, files, startedAt, completedAt };
|
|
168
|
+
}
|
|
169
|
+
if (type === "mcpToolCall" || type === "dynamicToolCall") {
|
|
170
|
+
const tool = stringValue(item.tool) ?? "tool";
|
|
171
|
+
const server = stringValue(item.server);
|
|
172
|
+
const title = server ? `${server}.${tool}` : tool;
|
|
173
|
+
const error = isRecord(item.error) ? stringValue(item.error.message) : undefined;
|
|
174
|
+
const argumentsText = formatJson(item.arguments);
|
|
175
|
+
const output = formatJson(type === "mcpToolCall" ? item.result : item.contentItems);
|
|
176
|
+
return {
|
|
177
|
+
id,
|
|
178
|
+
title,
|
|
179
|
+
kind: type === "mcpToolCall" ? "mcp" : "tool",
|
|
180
|
+
status,
|
|
181
|
+
command: argumentsText ? `${title}\n${argumentsText}` : title,
|
|
182
|
+
output,
|
|
183
|
+
error,
|
|
184
|
+
startedAt,
|
|
185
|
+
completedAt,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (type === "webSearch") {
|
|
189
|
+
return mapWebSearchTool(item, id, status, startedAt, completedAt);
|
|
190
|
+
}
|
|
191
|
+
if (type === "imageView") {
|
|
192
|
+
const imagePath = stringValue(item.path) ?? "image";
|
|
193
|
+
return {
|
|
194
|
+
id,
|
|
195
|
+
title: `查看图片 ${imagePath}`,
|
|
196
|
+
kind: "image_view",
|
|
197
|
+
status,
|
|
198
|
+
command: `view_image ${imagePath}`,
|
|
199
|
+
imagePath,
|
|
200
|
+
startedAt,
|
|
201
|
+
completedAt,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function mapWebSearchTool(
|
|
208
|
+
item: Record<string, unknown>,
|
|
209
|
+
id: string,
|
|
210
|
+
status: ToolState["status"],
|
|
211
|
+
startedAt?: number,
|
|
212
|
+
completedAt?: number,
|
|
213
|
+
): ToolState {
|
|
214
|
+
const action = isRecord(item.action) ? item.action : undefined;
|
|
215
|
+
const actionType = stringValue(action?.type);
|
|
216
|
+
const itemQuery = nonEmptyString(item.query);
|
|
217
|
+
|
|
218
|
+
if (actionType === "openPage" || actionType === "open_page") {
|
|
219
|
+
const url = nonEmptyString(action?.url);
|
|
220
|
+
const target = displayWebTarget(url);
|
|
221
|
+
return {
|
|
222
|
+
id,
|
|
223
|
+
title: target ? `打开网页 · ${target}` : "打开网页",
|
|
224
|
+
kind: "web_search",
|
|
225
|
+
status,
|
|
226
|
+
command: url ? `open_page ${url}` : "open_page",
|
|
227
|
+
startedAt,
|
|
228
|
+
completedAt,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (actionType === "findInPage" || actionType === "find_in_page") {
|
|
233
|
+
const url = nonEmptyString(action?.url);
|
|
234
|
+
const pattern = nonEmptyString(action?.pattern);
|
|
235
|
+
const target = displayWebTarget(url);
|
|
236
|
+
const summary = pattern ?? target;
|
|
237
|
+
return {
|
|
238
|
+
id,
|
|
239
|
+
title: summary ? `页内查找 · ${summary}` : "页内查找",
|
|
240
|
+
kind: "web_search",
|
|
241
|
+
status,
|
|
242
|
+
command: [
|
|
243
|
+
pattern ? `find_in_page ${JSON.stringify(pattern)}` : "find_in_page",
|
|
244
|
+
url,
|
|
245
|
+
].filter((part): part is string => part !== undefined).join("\n"),
|
|
246
|
+
startedAt,
|
|
247
|
+
completedAt,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const actionQueries = stringArray(action?.queries);
|
|
252
|
+
const actionQuery = nonEmptyString(action?.query);
|
|
253
|
+
const queries = actionQueries.length > 0
|
|
254
|
+
? actionQueries
|
|
255
|
+
: actionQuery
|
|
256
|
+
? [actionQuery]
|
|
257
|
+
: itemQuery
|
|
258
|
+
? [itemQuery]
|
|
259
|
+
: [];
|
|
260
|
+
const summary = queries.join(";");
|
|
261
|
+
return {
|
|
262
|
+
id,
|
|
263
|
+
title: summary ? `网页搜索 · ${summary}` : "网页搜索",
|
|
264
|
+
kind: "web_search",
|
|
265
|
+
status,
|
|
266
|
+
command: queries.length > 1
|
|
267
|
+
? `web_search\n${queries.map((query) => `- ${query}`).join("\n")}`
|
|
268
|
+
: queries[0]
|
|
269
|
+
? `web_search ${JSON.stringify(queries[0])}`
|
|
270
|
+
: "web_search",
|
|
271
|
+
startedAt,
|
|
272
|
+
completedAt,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function displayWebTarget(url: string | undefined): string | undefined {
|
|
277
|
+
if (!url) return undefined;
|
|
278
|
+
try {
|
|
279
|
+
const parsed = new URL(url);
|
|
280
|
+
const path = parsed.pathname === "/" ? "" : parsed.pathname;
|
|
281
|
+
return `${parsed.host}${path}`;
|
|
282
|
+
} catch {
|
|
283
|
+
return url;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
288
|
+
const text = stringValue(value)?.trim();
|
|
289
|
+
return text || undefined;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function stringArray(value: unknown): string[] {
|
|
293
|
+
return Array.isArray(value)
|
|
294
|
+
? value.flatMap((item) => {
|
|
295
|
+
const text = nonEmptyString(item);
|
|
296
|
+
return text ? [text] : [];
|
|
297
|
+
})
|
|
298
|
+
: [];
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function mapPlanStatus(value: unknown): PlanStep["status"] {
|
|
302
|
+
if (value === "completed") return "completed";
|
|
303
|
+
if (value === "inProgress" || value === "in_progress") return "in_progress";
|
|
304
|
+
return "pending";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function mapToolStatus(value: unknown, phase: "started" | "updated"): ToolState["status"] {
|
|
308
|
+
if (value === "completed") return "completed";
|
|
309
|
+
if (value === "failed" || value === "declined") return "failed";
|
|
310
|
+
return phase === "updated" ? "completed" : "running";
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function countDiff(diff: string, prefix: "+" | "-"): number {
|
|
314
|
+
return diff.split("\n").filter((line) => line.startsWith(prefix) && !line.startsWith(`${prefix}${prefix}${prefix}`)).length;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
318
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function stringValue(value: unknown): string | undefined {
|
|
322
|
+
return typeof value === "string" ? value : undefined;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function numberValue(value: unknown): number | undefined {
|
|
326
|
+
return typeof value === "number" ? value : undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function effectiveTokenCount(usage: Record<string, unknown>): number | undefined {
|
|
330
|
+
const inputTokens = numberValue(usage.inputTokens);
|
|
331
|
+
const cachedInputTokens = numberValue(usage.cachedInputTokens) ?? 0;
|
|
332
|
+
const outputTokens = numberValue(usage.outputTokens);
|
|
333
|
+
if (inputTokens !== undefined || outputTokens !== undefined) {
|
|
334
|
+
return Math.max(0, (inputTokens ?? 0) - cachedInputTokens) + Math.max(0, outputTokens ?? 0);
|
|
335
|
+
}
|
|
336
|
+
return numberValue(usage.totalTokens);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function secondsToMilliseconds(value: number | undefined): number | undefined {
|
|
340
|
+
return value === undefined ? undefined : value * 1_000;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function messagePhase(value: unknown): "commentary" | "final_answer" | undefined {
|
|
344
|
+
return value === "commentary" || value === "final_answer" ? value : undefined;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function formatJson(value: unknown): string | undefined {
|
|
348
|
+
if (value === undefined || value === null) return undefined;
|
|
349
|
+
if (typeof value === "string") return value;
|
|
350
|
+
return JSON.stringify(value, null, 2);
|
|
351
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { open } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import Database from "better-sqlite3";
|
|
4
|
+
import type { PermissionMode } from "../runtime/types.js";
|
|
5
|
+
|
|
6
|
+
const READ_CHUNK_SIZE = 64 * 1024;
|
|
7
|
+
const TASK_STARTED = Buffer.from('"type":"task_started"');
|
|
8
|
+
const TASK_COMPLETE = Buffer.from('"type":"task_complete"');
|
|
9
|
+
const TURN_ABORTED = Buffer.from('"type":"turn_aborted"');
|
|
10
|
+
const PATTERN_OVERLAP = Math.max(TASK_STARTED.length, TASK_COMPLETE.length, TURN_ABORTED.length) - 1;
|
|
11
|
+
|
|
12
|
+
interface ThreadPathRow {
|
|
13
|
+
id: string;
|
|
14
|
+
rollout_path: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ThreadSettingsRow {
|
|
18
|
+
id: string;
|
|
19
|
+
model: string | null;
|
|
20
|
+
reasoning_effort: string | null;
|
|
21
|
+
approval_mode: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CodexLocalThreadSettings {
|
|
25
|
+
model?: string;
|
|
26
|
+
reasoningEffort?: string;
|
|
27
|
+
permissionMode?: PermissionMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Reads Codex's persisted state without joining, resuming, or controlling a thread.
|
|
32
|
+
* This is needed because thread/list reports another app-server process as notLoaded
|
|
33
|
+
* even while Codex Desktop is actively appending events to its rollout.
|
|
34
|
+
*/
|
|
35
|
+
export class CodexLocalActivityDetector {
|
|
36
|
+
constructor(private readonly codexHome: string) {}
|
|
37
|
+
|
|
38
|
+
async activeThreadIds(threadIds: string[]): Promise<Set<string>> {
|
|
39
|
+
return new Set((await this.activeThreads(threadIds)).keys());
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async activeThreads(threadIds: string[]): Promise<Map<string, string | undefined>> {
|
|
43
|
+
const ids = [...new Set(threadIds.filter(Boolean))];
|
|
44
|
+
if (ids.length === 0) return new Map();
|
|
45
|
+
|
|
46
|
+
let database: Database.Database | undefined;
|
|
47
|
+
let rows: ThreadPathRow[];
|
|
48
|
+
try {
|
|
49
|
+
database = new Database(path.join(this.codexHome, "state_5.sqlite"), {
|
|
50
|
+
readonly: true,
|
|
51
|
+
fileMustExist: true,
|
|
52
|
+
});
|
|
53
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
54
|
+
rows = database.prepare(
|
|
55
|
+
`SELECT id, rollout_path FROM threads WHERE id IN (${placeholders})`,
|
|
56
|
+
).all(...ids) as ThreadPathRow[];
|
|
57
|
+
} catch {
|
|
58
|
+
return new Map();
|
|
59
|
+
} finally {
|
|
60
|
+
database?.close();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const active = new Map<string, string | undefined>();
|
|
64
|
+
for (const row of rows) {
|
|
65
|
+
const task = await latestTask(row.rollout_path);
|
|
66
|
+
if (task.active) active.set(row.id, task.turnId);
|
|
67
|
+
}
|
|
68
|
+
return active;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async threadSettings(threadIds: string[]): Promise<Map<string, CodexLocalThreadSettings>> {
|
|
72
|
+
const ids = [...new Set(threadIds.filter(Boolean))];
|
|
73
|
+
if (ids.length === 0) return new Map();
|
|
74
|
+
|
|
75
|
+
let database: Database.Database | undefined;
|
|
76
|
+
try {
|
|
77
|
+
database = new Database(path.join(this.codexHome, "state_5.sqlite"), {
|
|
78
|
+
readonly: true,
|
|
79
|
+
fileMustExist: true,
|
|
80
|
+
});
|
|
81
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
82
|
+
const rows = database.prepare(
|
|
83
|
+
`SELECT id, model, reasoning_effort, approval_mode FROM threads WHERE id IN (${placeholders})`,
|
|
84
|
+
).all(...ids) as ThreadSettingsRow[];
|
|
85
|
+
return new Map(rows.map((row) => [row.id, {
|
|
86
|
+
...(row.model?.trim() ? { model: row.model.trim() } : {}),
|
|
87
|
+
...(row.reasoning_effort?.trim() ? { reasoningEffort: row.reasoning_effort.trim() } : {}),
|
|
88
|
+
...(row.approval_mode?.trim()
|
|
89
|
+
? { permissionMode: row.approval_mode.trim() === "never" ? "auto" : "confirm" }
|
|
90
|
+
: {}),
|
|
91
|
+
}]));
|
|
92
|
+
} catch {
|
|
93
|
+
return new Map();
|
|
94
|
+
} finally {
|
|
95
|
+
database?.close();
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function latestTask(rolloutPath: string): Promise<{ active: boolean; turnId?: string }> {
|
|
101
|
+
let file;
|
|
102
|
+
try {
|
|
103
|
+
file = await open(rolloutPath, "r");
|
|
104
|
+
const size = (await file.stat()).size;
|
|
105
|
+
let position = size;
|
|
106
|
+
let laterPrefix = Buffer.alloc(0);
|
|
107
|
+
|
|
108
|
+
while (position > 0) {
|
|
109
|
+
const length = Math.min(READ_CHUNK_SIZE, position);
|
|
110
|
+
position -= length;
|
|
111
|
+
const chunk = Buffer.allocUnsafe(length);
|
|
112
|
+
await file.read(chunk, 0, length, position);
|
|
113
|
+
const searchable = laterPrefix.length > 0 ? Buffer.concat([chunk, laterPrefix]) : chunk;
|
|
114
|
+
const startedAt = searchable.lastIndexOf(TASK_STARTED);
|
|
115
|
+
const completedAt = searchable.lastIndexOf(TASK_COMPLETE);
|
|
116
|
+
const abortedAt = searchable.lastIndexOf(TURN_ABORTED);
|
|
117
|
+
const terminalAt = Math.max(completedAt, abortedAt);
|
|
118
|
+
if (startedAt >= 0 || terminalAt >= 0) {
|
|
119
|
+
if (startedAt <= terminalAt) return { active: false };
|
|
120
|
+
const absoluteOffset = position + startedAt;
|
|
121
|
+
return { active: true, turnId: await readTurnId(file, absoluteOffset, size) };
|
|
122
|
+
}
|
|
123
|
+
laterPrefix = chunk.subarray(0, Math.min(PATTERN_OVERLAP, chunk.length));
|
|
124
|
+
}
|
|
125
|
+
return { active: false };
|
|
126
|
+
} catch {
|
|
127
|
+
return { active: false };
|
|
128
|
+
} finally {
|
|
129
|
+
await file?.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function readTurnId(
|
|
134
|
+
file: Awaited<ReturnType<typeof open>>,
|
|
135
|
+
eventOffset: number,
|
|
136
|
+
fileSize: number,
|
|
137
|
+
): Promise<string | undefined> {
|
|
138
|
+
const start = Math.max(0, eventOffset - 1_024);
|
|
139
|
+
const length = Math.min(8_192, fileSize - start);
|
|
140
|
+
const buffer = Buffer.allocUnsafe(length);
|
|
141
|
+
await file.read(buffer, 0, length, start);
|
|
142
|
+
const relativeOffset = eventOffset - start;
|
|
143
|
+
const previousNewline = buffer.lastIndexOf(0x0a, relativeOffset);
|
|
144
|
+
const nextNewline = buffer.indexOf(0x0a, relativeOffset);
|
|
145
|
+
const line = buffer.subarray(previousNewline + 1, nextNewline >= 0 ? nextNewline : undefined).toString("utf8");
|
|
146
|
+
const match = /"turn_id":"([^"]+)"/.exec(line);
|
|
147
|
+
return match?.[1];
|
|
148
|
+
}
|