@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,324 @@
|
|
|
1
|
+
import type { JsonValue } from "../acp/acpTypes.js";
|
|
2
|
+
import { AcpSessionManager } from "../acp/AcpSessionManager.js";
|
|
3
|
+
import { createId } from "../utils/id.js";
|
|
4
|
+
import type {
|
|
5
|
+
AgentRuntime,
|
|
6
|
+
ApprovalDecision,
|
|
7
|
+
CreateRuntimeSessionInput,
|
|
8
|
+
ModelOption,
|
|
9
|
+
PermissionMode,
|
|
10
|
+
ResumeRuntimeSessionInput,
|
|
11
|
+
RuntimeSession,
|
|
12
|
+
RuntimeEvent,
|
|
13
|
+
RuntimePrompt,
|
|
14
|
+
RuntimeSessionMetadata,
|
|
15
|
+
ToolState,
|
|
16
|
+
} from "./types.js";
|
|
17
|
+
|
|
18
|
+
interface ActiveAcpTurn {
|
|
19
|
+
turnId: string;
|
|
20
|
+
finalText: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class AcpRuntimeAdapter implements AgentRuntime {
|
|
24
|
+
readonly kind = "acp" as const;
|
|
25
|
+
private readonly sessions = new Map<string, RuntimeSession>();
|
|
26
|
+
private readonly activeTurns = new Map<string, ActiveAcpTurn>();
|
|
27
|
+
private readonly toolsBySession = new Map<string, Map<string, ToolState>>();
|
|
28
|
+
private readonly listeners = new Set<(event: RuntimeEvent) => void>();
|
|
29
|
+
private readonly approvals = new Map<
|
|
30
|
+
string,
|
|
31
|
+
{ sessionId: string; resolve: (value: JsonValue) => void; optionIds: Map<ApprovalDecision, string> }
|
|
32
|
+
>();
|
|
33
|
+
|
|
34
|
+
constructor(private readonly acp: AcpSessionManager) {}
|
|
35
|
+
|
|
36
|
+
getSession(localSessionId: string): RuntimeSession | undefined {
|
|
37
|
+
return this.sessions.get(localSessionId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
41
|
+
const created = await this.acp.create({
|
|
42
|
+
localSessionId: input.localSessionId,
|
|
43
|
+
agentName: input.agentName,
|
|
44
|
+
cwd: input.cwd,
|
|
45
|
+
onUpdate: (_session, update) => this.handleUpdate(input.localSessionId, update),
|
|
46
|
+
onPermissionRequest: (_session, params) =>
|
|
47
|
+
new Promise<JsonValue>((resolve) => {
|
|
48
|
+
const requestId = createId("approval");
|
|
49
|
+
const active = this.activeTurns.get(input.localSessionId);
|
|
50
|
+
const optionIds = new Map<ApprovalDecision, string>();
|
|
51
|
+
for (const option of params.options) {
|
|
52
|
+
if (option.kind.startsWith("allow")) optionIds.set("accept", option.optionId);
|
|
53
|
+
if (option.kind.startsWith("reject") || option.kind.startsWith("deny")) {
|
|
54
|
+
optionIds.set("decline", option.optionId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.approvals.set(requestId, { sessionId: input.localSessionId, resolve, optionIds });
|
|
58
|
+
this.emit({
|
|
59
|
+
type: "approval_requested",
|
|
60
|
+
sessionId: input.localSessionId,
|
|
61
|
+
turnId: active?.turnId ?? "unknown",
|
|
62
|
+
request: {
|
|
63
|
+
id: requestId,
|
|
64
|
+
title: typeof params.toolCall.title === "string" ? params.toolCall.title : "ACP permission request",
|
|
65
|
+
options: [
|
|
66
|
+
{ id: "accept", label: "允许" },
|
|
67
|
+
{ id: "decline", label: "拒绝" },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
}),
|
|
72
|
+
});
|
|
73
|
+
const session: RuntimeSession = {
|
|
74
|
+
localSessionId: input.localSessionId,
|
|
75
|
+
remoteSessionId: created.acpSessionId,
|
|
76
|
+
runtimeKind: "acp",
|
|
77
|
+
agentName: input.agentName,
|
|
78
|
+
cwd: input.cwd,
|
|
79
|
+
title: input.title,
|
|
80
|
+
model: input.model,
|
|
81
|
+
reasoningEffort: input.reasoningEffort,
|
|
82
|
+
permissionMode: input.permissionMode,
|
|
83
|
+
};
|
|
84
|
+
this.sessions.set(input.localSessionId, session);
|
|
85
|
+
return session;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession> {
|
|
89
|
+
return this.createSession(input);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async readSessionMetadata(_remoteSessionId: string): Promise<RuntimeSessionMetadata> {
|
|
93
|
+
return {};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async synchronizeSession(sessionId: string): Promise<RuntimeSession> {
|
|
97
|
+
return this.requireSession(sessionId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string> {
|
|
101
|
+
const { text, localImagePaths } = normalizeRuntimePrompt(prompt);
|
|
102
|
+
if (localImagePaths.length > 0) throw new Error("当前 ACP Agent 不支持图片输入,请切换到 Codex 后重试。");
|
|
103
|
+
const session = this.requireSession(sessionId);
|
|
104
|
+
const turnId = createId("turn");
|
|
105
|
+
session.activeTurnId = turnId;
|
|
106
|
+
this.activeTurns.set(sessionId, { turnId, finalText: "" });
|
|
107
|
+
this.toolsBySession.set(sessionId, new Map());
|
|
108
|
+
this.emit({ type: "turn_started", sessionId, turnId, startedAt: Date.now() });
|
|
109
|
+
void this.acp.prompt(sessionId, text).then(
|
|
110
|
+
() => {
|
|
111
|
+
const active = this.activeTurns.get(sessionId);
|
|
112
|
+
if (!active || active.turnId !== turnId) return;
|
|
113
|
+
session.activeTurnId = undefined;
|
|
114
|
+
this.activeTurns.delete(sessionId);
|
|
115
|
+
this.toolsBySession.delete(sessionId);
|
|
116
|
+
this.emit({ type: "turn_completed", sessionId, turnId, finalResponse: active.finalText });
|
|
117
|
+
},
|
|
118
|
+
(error: unknown) => {
|
|
119
|
+
session.activeTurnId = undefined;
|
|
120
|
+
this.activeTurns.delete(sessionId);
|
|
121
|
+
this.toolsBySession.delete(sessionId);
|
|
122
|
+
this.emit({
|
|
123
|
+
type: "turn_failed",
|
|
124
|
+
sessionId,
|
|
125
|
+
turnId,
|
|
126
|
+
message: error instanceof Error ? error.message : String(error),
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
);
|
|
130
|
+
return turnId;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async steerTurn(_sessionId: string, _turnId: string, _prompt: RuntimePrompt): Promise<void> {
|
|
134
|
+
throw new Error("ACP runtime does not support steering an active turn.");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async cancelTurn(sessionId: string, turnId: string): Promise<void> {
|
|
138
|
+
this.acp.cancel(sessionId);
|
|
139
|
+
const session = this.requireSession(sessionId);
|
|
140
|
+
session.activeTurnId = undefined;
|
|
141
|
+
this.activeTurns.delete(sessionId);
|
|
142
|
+
this.toolsBySession.delete(sessionId);
|
|
143
|
+
this.emit({ type: "turn_cancelled", sessionId, turnId });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async closeSession(sessionId: string): Promise<void> {
|
|
147
|
+
await this.acp.close(sessionId);
|
|
148
|
+
this.sessions.delete(sessionId);
|
|
149
|
+
this.activeTurns.delete(sessionId);
|
|
150
|
+
this.toolsBySession.delete(sessionId);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async setTitle(sessionId: string, title: string): Promise<void> {
|
|
154
|
+
this.requireSession(sessionId).title = title;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async setModel(): Promise<void> {
|
|
158
|
+
throw new Error("ACP runtime does not expose model selection through Agent Bot.");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async setReasoningEffort(_sessionId: string, _effort: string): Promise<void> {
|
|
162
|
+
throw new Error("ACP runtime does not expose reasoning effort through Agent Bot.");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async setPermissionMode(sessionId: string, mode: PermissionMode): Promise<void> {
|
|
166
|
+
this.requireSession(sessionId).permissionMode = mode;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async respondToApproval(
|
|
170
|
+
sessionId: string,
|
|
171
|
+
requestId: string,
|
|
172
|
+
decision: ApprovalDecision,
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
const pending = this.approvals.get(requestId);
|
|
175
|
+
if (!pending || pending.sessionId !== sessionId) {
|
|
176
|
+
throw new Error("Approval request is no longer pending.");
|
|
177
|
+
}
|
|
178
|
+
this.approvals.delete(requestId);
|
|
179
|
+
const optionId = pending.optionIds.get(decision) ?? pending.optionIds.get(decision === "cancel" ? "decline" : "accept");
|
|
180
|
+
pending.resolve({ outcome: { outcome: "selected", optionId: optionId ?? decision } });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async listModels(): Promise<ModelOption[]> {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
onEvent(listener: (event: RuntimeEvent) => void): () => void {
|
|
188
|
+
this.listeners.add(listener);
|
|
189
|
+
return () => this.listeners.delete(listener);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
close(): void {
|
|
193
|
+
this.sessions.clear();
|
|
194
|
+
this.activeTurns.clear();
|
|
195
|
+
this.toolsBySession.clear();
|
|
196
|
+
this.approvals.clear();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private handleUpdate(sessionId: string, update: Record<string, JsonValue>): void {
|
|
200
|
+
const active = this.activeTurns.get(sessionId);
|
|
201
|
+
if (!active) return;
|
|
202
|
+
const updateType = update.sessionUpdate;
|
|
203
|
+
if (updateType === "agent_message_chunk" && isRecord(update.content) && typeof update.content.text === "string") {
|
|
204
|
+
active.finalText += update.content.text;
|
|
205
|
+
this.emit({ type: "agent_text_delta", sessionId, turnId: active.turnId, text: update.content.text });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (updateType === "tool_call" || updateType === "tool_call_update") {
|
|
209
|
+
const id = typeof update.toolCallId === "string" ? update.toolCallId : createId("tool");
|
|
210
|
+
const tools = this.toolsBySession.get(sessionId) ?? new Map<string, ToolState>();
|
|
211
|
+
this.toolsBySession.set(sessionId, tools);
|
|
212
|
+
const previous = tools.get(id);
|
|
213
|
+
const rawInput = recordField(update, "rawInput");
|
|
214
|
+
const command = stringField(rawInput, "command") ?? previous?.command;
|
|
215
|
+
const description = stringField(rawInput, "description");
|
|
216
|
+
const status = update.status === "failed" ? "failed" : update.status === "completed" ? "completed" : "running";
|
|
217
|
+
const output = extractToolOutput(update) ?? previous?.output;
|
|
218
|
+
const error = extractToolError(update, status, output) ?? previous?.error;
|
|
219
|
+
const tool: ToolState = {
|
|
220
|
+
...previous,
|
|
221
|
+
id,
|
|
222
|
+
title: description ?? (typeof update.title === "string" ? update.title : previous?.title) ?? command ?? "ACP tool",
|
|
223
|
+
kind: typeof update.kind === "string" ? update.kind : previous?.kind ?? (command ? "command" : "tool"),
|
|
224
|
+
status,
|
|
225
|
+
command,
|
|
226
|
+
output: status === "failed" ? previous?.output : output,
|
|
227
|
+
error,
|
|
228
|
+
exitCode: extractExitCode(update) ?? previous?.exitCode,
|
|
229
|
+
};
|
|
230
|
+
tools.set(id, tool);
|
|
231
|
+
this.emit({
|
|
232
|
+
type: updateType === "tool_call" ? "tool_started" : "tool_updated",
|
|
233
|
+
sessionId,
|
|
234
|
+
turnId: active.turnId,
|
|
235
|
+
tool,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private requireSession(sessionId: string): RuntimeSession {
|
|
241
|
+
const session = this.sessions.get(sessionId);
|
|
242
|
+
if (!session) throw new Error(`Unknown runtime session: ${sessionId}`);
|
|
243
|
+
return session;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
private emit(event: RuntimeEvent): void {
|
|
247
|
+
for (const listener of this.listeners) listener(event);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function isRecord(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
252
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function recordField(record: Record<string, JsonValue> | undefined, key: string): Record<string, JsonValue> | undefined {
|
|
256
|
+
const value = field(record, key);
|
|
257
|
+
return isRecord(value) ? value : undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stringField(record: Record<string, JsonValue> | undefined, key: string): string | undefined {
|
|
261
|
+
const value = field(record, key);
|
|
262
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function numberField(record: Record<string, JsonValue> | undefined, key: string): number | undefined {
|
|
266
|
+
const value = field(record, key);
|
|
267
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function field(record: Record<string, JsonValue> | undefined, key: string): JsonValue | undefined {
|
|
271
|
+
if (!record) return undefined;
|
|
272
|
+
const normalizedKey = normalizeKey(key);
|
|
273
|
+
const entry = Object.entries(record).find(([candidate]) => normalizeKey(candidate) === normalizedKey);
|
|
274
|
+
return entry?.[1];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function normalizeKey(value: string): string {
|
|
278
|
+
return value.replaceAll(/[_-]/g, "").toLowerCase();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function extractToolOutput(update: Record<string, JsonValue>): string | undefined {
|
|
282
|
+
const content = update.content;
|
|
283
|
+
if (Array.isArray(content)) {
|
|
284
|
+
const text = content
|
|
285
|
+
.flatMap((item) => {
|
|
286
|
+
if (!isRecord(item)) return [];
|
|
287
|
+
const nested = recordField(item, "content");
|
|
288
|
+
const value = stringField(nested, "text") ?? stringField(item, "text");
|
|
289
|
+
return value ? [value] : [];
|
|
290
|
+
})
|
|
291
|
+
.join("\n")
|
|
292
|
+
.trim();
|
|
293
|
+
if (text) return text;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const output = recordField(recordField(update, "rawOutput"), "output") ?? recordField(update, "rawOutput");
|
|
297
|
+
const stdout = stringField(output, "stdout");
|
|
298
|
+
const stderr = stringField(output, "stderr");
|
|
299
|
+
const combined = [stdout, stderr].filter((value): value is string => Boolean(value)).join("\n").trim();
|
|
300
|
+
return combined || undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function extractToolError(
|
|
304
|
+
update: Record<string, JsonValue>,
|
|
305
|
+
status: ToolState["status"],
|
|
306
|
+
output: string | undefined,
|
|
307
|
+
): string | undefined {
|
|
308
|
+
const direct = typeof update.error === "string" ? update.error : undefined;
|
|
309
|
+
if (direct) return direct;
|
|
310
|
+
if (status !== "failed") return undefined;
|
|
311
|
+
const outputRecord = recordField(recordField(update, "rawOutput"), "output") ?? recordField(update, "rawOutput");
|
|
312
|
+
return stringField(outputRecord, "stderr") ?? output;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function extractExitCode(update: Record<string, JsonValue>): number | undefined {
|
|
316
|
+
const output = recordField(recordField(update, "rawOutput"), "output") ?? recordField(update, "rawOutput");
|
|
317
|
+
return numberField(output, "exitCode") ?? numberField(output, "code");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function normalizeRuntimePrompt(prompt: RuntimePrompt): { text: string; localImagePaths: string[] } {
|
|
321
|
+
return typeof prompt === "string"
|
|
322
|
+
? { text: prompt, localImagePaths: [] }
|
|
323
|
+
: { text: prompt.text, localImagePaths: prompt.localImagePaths ?? [] };
|
|
324
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentConfig } from "../config/schema.js";
|
|
2
|
+
import type { AgentRuntime, RuntimeKind } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export class AgentRuntimeRegistry {
|
|
5
|
+
constructor(private readonly runtimes: Record<RuntimeKind, AgentRuntime>) {}
|
|
6
|
+
|
|
7
|
+
forAgent(agent: Pick<AgentConfig, "kind">): AgentRuntime {
|
|
8
|
+
return this.runtimes[agent.kind];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
get(kind: RuntimeKind): AgentRuntime {
|
|
12
|
+
return this.runtimes[kind];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
close(): void {
|
|
16
|
+
for (const runtime of Object.values(this.runtimes)) {
|
|
17
|
+
runtime.close();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const AGENT_BOT_ENVIRONMENT = Object.freeze({
|
|
2
|
+
AGENT_BOT: "1",
|
|
3
|
+
});
|
|
4
|
+
|
|
5
|
+
export function agentBotEnvironment(
|
|
6
|
+
baseEnv: NodeJS.ProcessEnv,
|
|
7
|
+
agentEnv: Record<string, string>,
|
|
8
|
+
): NodeJS.ProcessEnv {
|
|
9
|
+
return {
|
|
10
|
+
...baseEnv,
|
|
11
|
+
...agentEnv,
|
|
12
|
+
...AGENT_BOT_ENVIRONMENT,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export type RuntimeKind = "acp" | "codex";
|
|
2
|
+
export type PermissionMode = "auto" | "confirm";
|
|
3
|
+
export type ApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
|
|
4
|
+
export type ToolStatus = "running" | "completed" | "failed";
|
|
5
|
+
export type RuntimeGoalStatus =
|
|
6
|
+
| "active"
|
|
7
|
+
| "paused"
|
|
8
|
+
| "blocked"
|
|
9
|
+
| "usageLimited"
|
|
10
|
+
| "budgetLimited"
|
|
11
|
+
| "complete";
|
|
12
|
+
|
|
13
|
+
export interface RuntimeGoal {
|
|
14
|
+
threadId: string;
|
|
15
|
+
objective: string;
|
|
16
|
+
status: RuntimeGoalStatus;
|
|
17
|
+
tokenBudget?: number | null;
|
|
18
|
+
tokensUsed: number;
|
|
19
|
+
timeUsedSeconds: number;
|
|
20
|
+
createdAt: number;
|
|
21
|
+
updatedAt: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RuntimeGoalUpdate {
|
|
25
|
+
objective?: string;
|
|
26
|
+
status?: RuntimeGoalStatus;
|
|
27
|
+
tokenBudget?: number | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type RuntimePrompt = string | {
|
|
31
|
+
text: string;
|
|
32
|
+
localImagePaths?: string[];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface PlanStep {
|
|
36
|
+
text: string;
|
|
37
|
+
status: "pending" | "in_progress" | "completed";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ToolState {
|
|
41
|
+
id: string;
|
|
42
|
+
title: string;
|
|
43
|
+
kind: string;
|
|
44
|
+
status: ToolStatus;
|
|
45
|
+
command?: string;
|
|
46
|
+
output?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
exitCode?: number;
|
|
49
|
+
startedAt?: number;
|
|
50
|
+
completedAt?: number;
|
|
51
|
+
imagePath?: string;
|
|
52
|
+
files?: Array<{ path: string; additions?: number; deletions?: number }>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ApprovalRequest {
|
|
56
|
+
id: string;
|
|
57
|
+
title: string;
|
|
58
|
+
command?: string;
|
|
59
|
+
reason?: string;
|
|
60
|
+
options: Array<{ id: ApprovalDecision; label: string }>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type AgentEvent =
|
|
64
|
+
| { type: "turn_started"; sessionId: string; turnId: string; startedAt: number }
|
|
65
|
+
| { type: "agent_text_delta"; sessionId: string; turnId: string; text: string }
|
|
66
|
+
| {
|
|
67
|
+
type: "token_usage_updated";
|
|
68
|
+
sessionId: string;
|
|
69
|
+
turnId: string;
|
|
70
|
+
lastTokens: number;
|
|
71
|
+
cumulativeTokens: number;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
type: "progress";
|
|
75
|
+
sessionId: string;
|
|
76
|
+
turnId: string;
|
|
77
|
+
text: string;
|
|
78
|
+
activityId?: string;
|
|
79
|
+
append?: boolean;
|
|
80
|
+
}
|
|
81
|
+
| { type: "plan_updated"; sessionId: string; turnId: string; steps: PlanStep[] }
|
|
82
|
+
| { type: "tool_started"; sessionId: string; turnId: string; tool: ToolState }
|
|
83
|
+
| { type: "tool_updated"; sessionId: string; turnId: string; tool: ToolState }
|
|
84
|
+
| { type: "tool_output_delta"; sessionId: string; turnId: string; toolId: string; delta: string }
|
|
85
|
+
| { type: "approval_requested"; sessionId: string; turnId: string; request: ApprovalRequest }
|
|
86
|
+
| { type: "approval_resolved"; sessionId: string; turnId: string; requestId: string; decision: ApprovalDecision }
|
|
87
|
+
| { type: "turn_completed"; sessionId: string; turnId: string; finalResponse: string; durationMs?: number }
|
|
88
|
+
| { type: "turn_cancelled"; sessionId: string; turnId: string }
|
|
89
|
+
| { type: "turn_failed"; sessionId: string; turnId: string; message: string };
|
|
90
|
+
|
|
91
|
+
export interface SessionMetadataUpdatedEvent {
|
|
92
|
+
type: "session_metadata_updated";
|
|
93
|
+
sessionId: string;
|
|
94
|
+
title: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export type RuntimeEvent = AgentEvent | SessionMetadataUpdatedEvent;
|
|
98
|
+
|
|
99
|
+
export interface RuntimeSessionMetadata {
|
|
100
|
+
title?: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export type RemoteSessionStatus = "active" | "idle" | "not_loaded" | "error";
|
|
104
|
+
|
|
105
|
+
export interface RemoteSessionSummary {
|
|
106
|
+
id: string;
|
|
107
|
+
title?: string;
|
|
108
|
+
preview?: string;
|
|
109
|
+
cwd: string;
|
|
110
|
+
source: string;
|
|
111
|
+
status: RemoteSessionStatus;
|
|
112
|
+
model?: string;
|
|
113
|
+
reasoningEffort?: string;
|
|
114
|
+
permissionMode?: PermissionMode;
|
|
115
|
+
createdAt?: number;
|
|
116
|
+
updatedAt?: number;
|
|
117
|
+
recencyAt?: number;
|
|
118
|
+
lastTurnId?: string;
|
|
119
|
+
lastCompletedTurnId?: string;
|
|
120
|
+
lastTurnStatus?: "completed" | "interrupted" | "failed" | "inProgress";
|
|
121
|
+
lastActivity?: string;
|
|
122
|
+
finalResponse?: string;
|
|
123
|
+
lastError?: string;
|
|
124
|
+
lastTurnToolCount?: number;
|
|
125
|
+
lastTurnCompletedToolCount?: number;
|
|
126
|
+
lastTurnFailedToolCount?: number;
|
|
127
|
+
lastTurnRunningToolCount?: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface RemoteSessionPage {
|
|
131
|
+
sessions: RemoteSessionSummary[];
|
|
132
|
+
nextCursor?: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface RemoteSessionActivity {
|
|
136
|
+
active: boolean;
|
|
137
|
+
activeTurnId?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface RuntimeSession {
|
|
141
|
+
localSessionId: string;
|
|
142
|
+
remoteSessionId: string;
|
|
143
|
+
runtimeKind: RuntimeKind;
|
|
144
|
+
agentName: string;
|
|
145
|
+
cwd: string;
|
|
146
|
+
title?: string;
|
|
147
|
+
model?: string;
|
|
148
|
+
reasoningEffort?: string;
|
|
149
|
+
permissionMode: PermissionMode;
|
|
150
|
+
activeTurnId?: string;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface CreateRuntimeSessionInput {
|
|
154
|
+
localSessionId: string;
|
|
155
|
+
agentName: string;
|
|
156
|
+
cwd: string;
|
|
157
|
+
title?: string;
|
|
158
|
+
model?: string;
|
|
159
|
+
reasoningEffort?: string;
|
|
160
|
+
permissionMode: PermissionMode;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface ResumeRuntimeSessionInput extends CreateRuntimeSessionInput {
|
|
164
|
+
remoteSessionId: string;
|
|
165
|
+
activeTurnId?: string;
|
|
166
|
+
lastTurnId?: string;
|
|
167
|
+
lastTurnStatus?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ForkRuntimeSessionInput extends CreateRuntimeSessionInput {
|
|
171
|
+
remoteSessionId: string;
|
|
172
|
+
lastTurnId: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface ModelOption {
|
|
176
|
+
id: string;
|
|
177
|
+
displayName?: string;
|
|
178
|
+
isDefault?: boolean;
|
|
179
|
+
supportedReasoningEfforts: ReasoningEffortOption[];
|
|
180
|
+
defaultReasoningEffort?: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface ReasoningEffortOption {
|
|
184
|
+
value: string;
|
|
185
|
+
description?: string;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface AgentRuntime {
|
|
189
|
+
readonly kind: RuntimeKind;
|
|
190
|
+
createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession>;
|
|
191
|
+
resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession>;
|
|
192
|
+
forkSession?(input: ForkRuntimeSessionInput): Promise<RuntimeSession>;
|
|
193
|
+
getSession(localSessionId: string): RuntimeSession | undefined;
|
|
194
|
+
readSessionMetadata(remoteSessionId: string): Promise<RuntimeSessionMetadata>;
|
|
195
|
+
listRemoteSessions?(input?: { searchTerm?: string; cursor?: string; limit?: number }): Promise<RemoteSessionPage>;
|
|
196
|
+
readRemoteSession?(remoteSessionId: string): Promise<RemoteSessionSummary>;
|
|
197
|
+
inspectRemoteSessionActivity?(remoteSessionId: string): Promise<RemoteSessionActivity>;
|
|
198
|
+
synchronizeSession(sessionId: string): Promise<RuntimeSession>;
|
|
199
|
+
startTurn(sessionId: string, prompt: RuntimePrompt): Promise<string>;
|
|
200
|
+
steerTurn(sessionId: string, turnId: string, prompt: RuntimePrompt): Promise<void>;
|
|
201
|
+
cancelTurn(sessionId: string, turnId: string): Promise<void>;
|
|
202
|
+
interruptRemoteTurn?(remoteSessionId: string, turnId: string): Promise<void>;
|
|
203
|
+
closeSession(sessionId: string): Promise<void>;
|
|
204
|
+
setTitle?(sessionId: string, title: string): Promise<void>;
|
|
205
|
+
getGoal?(sessionId: string): Promise<RuntimeGoal | undefined>;
|
|
206
|
+
setGoal?(sessionId: string, update: RuntimeGoalUpdate): Promise<RuntimeGoal>;
|
|
207
|
+
clearGoal?(sessionId: string): Promise<boolean>;
|
|
208
|
+
setModel(sessionId: string, model: string): Promise<void>;
|
|
209
|
+
setReasoningEffort(sessionId: string, effort: string): Promise<void>;
|
|
210
|
+
setPermissionMode(sessionId: string, mode: PermissionMode): Promise<void>;
|
|
211
|
+
respondToApproval(sessionId: string, requestId: string, decision: ApprovalDecision): Promise<void>;
|
|
212
|
+
listModels(): Promise<ModelOption[]>;
|
|
213
|
+
onEvent(listener: (event: RuntimeEvent) => void): () => void;
|
|
214
|
+
close(): void;
|
|
215
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AgentRuntimeRegistry } from "../runtime/AgentRuntimeRegistry.js";
|
|
2
|
+
import { StateStore, type SessionRecord } from "../state/StateStore.js";
|
|
3
|
+
import { normalizeTaskTitle } from "../utils/taskTitle.js";
|
|
4
|
+
|
|
5
|
+
export class SessionMetadataHydrator {
|
|
6
|
+
constructor(
|
|
7
|
+
private readonly store: StateStore,
|
|
8
|
+
private readonly runtimes: AgentRuntimeRegistry,
|
|
9
|
+
) {}
|
|
10
|
+
|
|
11
|
+
async hydrate(session: SessionRecord): Promise<SessionRecord> {
|
|
12
|
+
if (
|
|
13
|
+
session.runtimeKind === "codex"
|
|
14
|
+
&& session.remoteSessionId
|
|
15
|
+
&& session.lastTurnId
|
|
16
|
+
&& session.status === "running"
|
|
17
|
+
&& session.lastTurnStatus === "running"
|
|
18
|
+
) {
|
|
19
|
+
const runtime = this.runtimes.get("codex");
|
|
20
|
+
const loaded = runtime.getSession(session.localSessionId) ?? await runtime.resumeSession({
|
|
21
|
+
localSessionId: session.localSessionId,
|
|
22
|
+
remoteSessionId: session.remoteSessionId,
|
|
23
|
+
activeTurnId: session.lastTurnId,
|
|
24
|
+
lastTurnId: session.lastTurnId,
|
|
25
|
+
lastTurnStatus: session.lastTurnStatus,
|
|
26
|
+
agentName: session.agentName,
|
|
27
|
+
cwd: session.cwd,
|
|
28
|
+
title: session.title,
|
|
29
|
+
model: session.model,
|
|
30
|
+
reasoningEffort: session.reasoningEffort,
|
|
31
|
+
permissionMode: session.permissionMode ?? "auto",
|
|
32
|
+
});
|
|
33
|
+
const synchronized = await runtime.synchronizeSession(loaded.localSessionId);
|
|
34
|
+
if (synchronized.title) {
|
|
35
|
+
this.store.updateRuntimeSession(session.localSessionId, { title: synchronized.title });
|
|
36
|
+
}
|
|
37
|
+
return this.store.getSession(session.localSessionId) ?? session;
|
|
38
|
+
}
|
|
39
|
+
if (session.title || !session.runtimeKind || !session.remoteSessionId) return session;
|
|
40
|
+
if (session.runtimeKind === "codex" && !session.lastTurnId) return session;
|
|
41
|
+
const metadata = await this.runtimes.get(session.runtimeKind).readSessionMetadata(session.remoteSessionId);
|
|
42
|
+
const title = normalizeTaskTitle(metadata.title);
|
|
43
|
+
if (title) this.store.updateRuntimeSession(session.localSessionId, { title });
|
|
44
|
+
return this.store.getSession(session.localSessionId) ?? session;
|
|
45
|
+
}
|
|
46
|
+
}
|