@keyou007/agent-bot 0.1.13-alpha.1 → 0.1.13-alpha.2

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +106 -103
  3. package/README.zh.md +76 -73
  4. package/dist/acp/AcpProcessManager.js +8 -3
  5. package/dist/acp/AcpProcessManager.js.map +1 -1
  6. package/dist/acp/AcpSessionManager.js +15 -0
  7. package/dist/acp/AcpSessionManager.js.map +1 -1
  8. package/dist/cli/help.js +4 -2
  9. package/dist/cli/help.js.map +1 -1
  10. package/dist/cli/serverStatus.js +27 -0
  11. package/dist/cli/serverStatus.js.map +1 -1
  12. package/dist/cli/taskListOutput.js +72 -0
  13. package/dist/cli/taskListOutput.js.map +1 -0
  14. package/dist/cli.js +49 -54
  15. package/dist/cli.js.map +1 -1
  16. package/dist/codex/CodexProcessManager.js +34 -4
  17. package/dist/codex/CodexProcessManager.js.map +1 -1
  18. package/dist/codex/CodexRuntime.js +3 -0
  19. package/dist/codex/CodexRuntime.js.map +1 -1
  20. package/dist/feishu/CardRenderer.js +43 -2
  21. package/dist/feishu/CardRenderer.js.map +1 -1
  22. package/dist/feishu/FeishuConnector.js +16 -5
  23. package/dist/feishu/FeishuConnector.js.map +1 -1
  24. package/dist/index.js +23 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/proxy/ProxySessionController.js +157 -59
  27. package/dist/proxy/ProxySessionController.js.map +1 -1
  28. package/dist/runtime/AcpRuntimeAdapter.js +3 -0
  29. package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
  30. package/dist/runtime/agentEnvironment.js +24 -4
  31. package/dist/runtime/agentEnvironment.js.map +1 -1
  32. package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
  33. package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
  34. package/dist/state/StateStore.js +12 -9
  35. package/dist/state/StateStore.js.map +1 -1
  36. package/docs/technical-reference.md +9 -3
  37. package/docs/technical-reference.zh.md +9 -3
  38. package/npm-shrinkwrap.json +2 -2
  39. package/package.json +1 -1
  40. package/skills/agent-bot/SKILL.md +53 -22
  41. package/src/acp/AcpProcessManager.ts +16 -3
  42. package/src/acp/AcpSessionManager.ts +14 -0
  43. package/src/cli/help.ts +4 -2
  44. package/src/cli/serverStatus.ts +28 -0
  45. package/src/cli/taskListOutput.ts +91 -0
  46. package/src/cli.ts +66 -51
  47. package/src/codex/CodexProcessManager.ts +38 -3
  48. package/src/codex/CodexRuntime.ts +6 -0
  49. package/src/feishu/CardRenderer.ts +61 -1
  50. package/src/feishu/FeishuConnector.ts +13 -4
  51. package/src/index.ts +35 -3
  52. package/src/proxy/ProxySessionController.ts +186 -59
  53. package/src/runtime/AcpRuntimeAdapter.ts +5 -0
  54. package/src/runtime/agentEnvironment.ts +41 -3
  55. package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
  56. package/src/runtime/types.ts +5 -0
  57. package/src/state/StateStore.ts +12 -9
package/src/index.ts CHANGED
@@ -6,7 +6,12 @@ import { controlEndpoint, type ControlRequest, type ControlResponse } from "./cl
6
6
  import { readPackageVersion } from "./cli/packageVersion.js";
7
7
  import { loadConfig } from "./config/loadConfig.js";
8
8
  import { persistFeishuUserOpenIdIfMissing } from "./config/FeishuUserOpenIdStore.js";
9
- import { defaultDotEnvPath } from "./config/paths.js";
9
+ import {
10
+ agentBotHome,
11
+ defaultConfigPath,
12
+ defaultDotEnvPath,
13
+ resolveUserPath,
14
+ } from "./config/paths.js";
10
15
  import { ConsoleConnector } from "./console/ConsoleConnector.js";
11
16
  import { ConsoleTurnPresenter } from "./console/ConsoleTurnPresenter.js";
12
17
  import { CardRenderer } from "./feishu/CardRenderer.js";
@@ -20,6 +25,7 @@ import { requireServerFeishuTransport } from "./feishu/transport.js";
20
25
  import { createLogger } from "./logging/logger.js";
21
26
  import { OutboundRouter, type OutboundRoute } from "./presentation/OutboundRouter.js";
22
27
  import { ProxySessionController } from "./proxy/ProxySessionController.js";
28
+ import type { AgentEnvironmentContext } from "./runtime/agentEnvironment.js";
23
29
  import { createAgentRuntimeRegistry } from "./runtime/createAgentRuntimeRegistry.js";
24
30
  import { StateStore } from "./state/StateStore.js";
25
31
  import { StartupNotifier } from "./startup/StartupNotifier.js";
@@ -55,7 +61,14 @@ const transport = consoleOnly ? "console" : requireServerFeishuTransport(config.
55
61
  const logger = createLogger(config);
56
62
  const store = new StateStore(config.storage.sqlitePath);
57
63
 
58
- const runtimes = createAgentRuntimeRegistry(config, logger);
64
+ const configuredConfigPath = process.env.AGENT_BOT_CONFIG?.trim();
65
+ const agentEnvironmentContext: AgentEnvironmentContext = {
66
+ profilePath: agentBotHome(),
67
+ configPath: configuredConfigPath ? resolveUserPath(configuredConfigPath) : defaultConfigPath(),
68
+ larkAppId: config.feishu.appId,
69
+ larkUserOpenId: config.feishu.userOpenId,
70
+ };
71
+ const runtimes = createAgentRuntimeRegistry(config, logger, agentEnvironmentContext);
59
72
 
60
73
  const routes: OutboundRoute[] = [];
61
74
  let feishuConnector: FeishuConnector | undefined;
@@ -116,6 +129,7 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
116
129
  if (config.feishu.userOpenId?.startsWith("ou_")) return;
117
130
  const persisted = persistFeishuUserOpenIdIfMissing(defaultDotEnvPath(), userOpenId);
118
131
  config.feishu.userOpenId = persisted.userOpenId;
132
+ agentEnvironmentContext.larkUserOpenId = persisted.userOpenId;
119
133
  process.env.FEISHU_USER_OPEN_ID = persisted.userOpenId;
120
134
  logger.info(
121
135
  { status: persisted.status },
@@ -124,7 +138,15 @@ const controller = new ProxySessionController(config, store, runtimes, outbound,
124
138
  },
125
139
  });
126
140
 
127
- if (feishuOutbound) feishuConnector = new FeishuConnector(config, controller, logger);
141
+ if (feishuOutbound) {
142
+ feishuConnector = new FeishuConnector(
143
+ config,
144
+ controller,
145
+ logger,
146
+ undefined,
147
+ (botOpenId) => { agentEnvironmentContext.larkBotOpenId = botOpenId; },
148
+ );
149
+ }
128
150
  if (consoleEnabled) consoleConnector = new ConsoleConnector(controller, logger);
129
151
 
130
152
  process.on("SIGINT", () => void shutdown(0));
@@ -224,6 +246,16 @@ async function handleControlRequest(request: ControlRequest): Promise<ControlRes
224
246
  startedAt: processStartedAt.toISOString(),
225
247
  supervised,
226
248
  feishuAppId: config.feishu.appId ?? null,
249
+ agents: runtimes.entries().map(([name, runtime]) => {
250
+ const processInfo = runtime.getProcessInfo?.() ?? {};
251
+ return {
252
+ name,
253
+ title: config.agents[name]?.title ?? name,
254
+ kind: config.agents[name]?.kind ?? runtime.kind,
255
+ pid: processInfo.pid ?? null,
256
+ version: processInfo.version ?? null,
257
+ };
258
+ }),
227
259
  safeRestartScheduled: safeRestart.scheduled,
228
260
  safeRestartReason: safeRestart.pendingReason,
229
261
  activity: store.getServerActivityState(),
@@ -17,6 +17,7 @@ import {
17
17
  CardRenderer,
18
18
  type CardSection,
19
19
  type ExecutionSettingsTab,
20
+ type HelpCardSection,
20
21
  type ResetHistoryCardEntry,
21
22
  type TaskListCardAction,
22
23
  type TaskListCardEntry,
@@ -67,6 +68,123 @@ const SESSION_PAGE_SIZE = 5;
67
68
  const RESET_HISTORY_PAGE_SIZE = 10;
68
69
  const REMOTE_SESSION_REFERENCE_PREFIX = "agent-runtime:";
69
70
 
71
+ interface HelpCommandDefinition {
72
+ command: string;
73
+ usage?: string;
74
+ description: string;
75
+ requiresArgument?: boolean;
76
+ }
77
+
78
+ const HELP_COMMAND_SECTIONS: Array<{
79
+ title: string;
80
+ commands: HelpCommandDefinition[];
81
+ }> = [
82
+ {
83
+ title: "任务管理",
84
+ commands: [
85
+ {
86
+ command: "/new",
87
+ usage: "[title] [--dir &#60;cwd&#62; | --nodir]",
88
+ description: "使用默认 Agent 创建任务;--nodir 创建 Projectless 任务",
89
+ },
90
+ {
91
+ command: "/newgroup",
92
+ usage: "[title] [--dir &#60;cwd&#62; | --nodir]",
93
+ description: "创建飞书群和新任务",
94
+ },
95
+ {
96
+ command: "/forkgroup",
97
+ usage: "[title]",
98
+ description: "从当前任务最近完成轮次创建分支群",
99
+ },
100
+ {
101
+ command: "/fork",
102
+ usage: "[序号或任务 ID]",
103
+ description: "从当前或指定任务最近完成轮次创建分支",
104
+ },
105
+ { command: "/turns", description: "浏览历史轮次,并可 Reset 对话上下文" },
106
+ {
107
+ command: "/title",
108
+ usage: "&#60;新标题&#62;",
109
+ description: "修改当前任务标题",
110
+ requiresArgument: true,
111
+ },
112
+ { command: "/sessions", usage: "[关键词]", description: "查找本机任务" },
113
+ {
114
+ command: "/switch",
115
+ usage: "[序号或任务 ID]",
116
+ description: "切换任务;不填参数切回上一个任务",
117
+ },
118
+ ],
119
+ },
120
+ {
121
+ title: "执行设置",
122
+ commands: [
123
+ {
124
+ command: "!",
125
+ usage: "&#60;命令&#62;",
126
+ description: "在当前任务目录执行本地命令",
127
+ requiresArgument: true,
128
+ },
129
+ { command: "/stop", description: "停止当前执行" },
130
+ {
131
+ command: "/queue",
132
+ usage: "&#60;prompt&#62;",
133
+ description: "将 Prompt 排队为后续轮次",
134
+ requiresArgument: true,
135
+ },
136
+ {
137
+ command: "/nosteer",
138
+ usage: "&#60;prompt&#62;",
139
+ description: "与 /queue 相同",
140
+ requiresArgument: true,
141
+ },
142
+ {
143
+ command: "/goal",
144
+ usage: "&#60;目标或操作&#62;",
145
+ description: "查看或创建长任务 Goal;支持 pause、resume、edit、clear",
146
+ requiresArgument: true,
147
+ },
148
+ { command: "/provider", description: "选择 AI 服务提供商" },
149
+ { command: "/model", description: "选择当前任务使用的模型" },
150
+ { command: "/thinking", description: "设置模型的思考强度" },
151
+ { command: "/permissions", description: "设置执行工具前是否需要确认" },
152
+ ],
153
+ },
154
+ {
155
+ title: "Agent",
156
+ commands: [
157
+ {
158
+ command: "/agent",
159
+ usage: "[name]",
160
+ description: "选择新任务使用的默认 Agent",
161
+ },
162
+ ],
163
+ },
164
+ {
165
+ title: "系统",
166
+ commands: [
167
+ {
168
+ command: "/status",
169
+ usage: "[序号或任务 ID]",
170
+ description: "查看当前或指定任务状态",
171
+ },
172
+ {
173
+ command: "/restart",
174
+ usage: "[--force]",
175
+ description: "默认安全重启;--force 立即重启",
176
+ },
177
+ { command: "/help", description: "显示本帮助" },
178
+ ],
179
+ },
180
+ ];
181
+
182
+ const HELP_DEFAULT_COMMANDS = new Set(
183
+ HELP_COMMAND_SECTIONS.flatMap((section) => section.commands)
184
+ .filter((command) => !command.requiresArgument)
185
+ .map((command) => command.command),
186
+ );
187
+
70
188
  interface AgentRemoteSession {
71
189
  agentName: string;
72
190
  runtime: AgentRuntime;
@@ -365,7 +483,9 @@ export class ProxySessionController {
365
483
  await this.outbound.withReplyTarget(contextKey, replyTarget, async () => {
366
484
  try {
367
485
  const kind = String(scopedAction.value.action ?? "");
368
- if (kind === "turn_details") {
486
+ if (kind === "help_command") {
487
+ await this.executeHelpCommandAction(scopedAction, contextKey, replyTarget);
488
+ } else if (kind === "turn_details") {
369
489
  await this.outbound.showDetails(contextKey, String(scopedAction.value.turnId ?? ""));
370
490
  } else if (kind === "activity_history") {
371
491
  const requestedPage = String(scopedAction.value.page ?? "0");
@@ -2111,7 +2231,7 @@ export class ProxySessionController {
2111
2231
  this.store.saveTurnParent(
2112
2232
  event.turnId,
2113
2233
  event.sessionId,
2114
- source?.lastTurnStatus === "completed" ? source.lastTurnId : undefined,
2234
+ this.completedParentTurnId(source),
2115
2235
  );
2116
2236
  }
2117
2237
  }
@@ -2209,6 +2329,13 @@ export class ProxySessionController {
2209
2329
  }
2210
2330
  }
2211
2331
 
2332
+ private completedParentTurnId(source: SessionRecord | undefined): string | undefined {
2333
+ if (!source?.lastTurnId) return undefined;
2334
+ if (source.lastTurnStatus === "completed") return source.lastTurnId;
2335
+ const snapshot = turnViewSnapshot(this.store.getTurnSnapshot(source.lastTurnId));
2336
+ return snapshot?.status === "completed" ? source.lastTurnId : undefined;
2337
+ }
2338
+
2212
2339
  private async finalizeStandaloneMessageReaction(
2213
2340
  messageId: string,
2214
2341
  status: "completed" | "failed" | "cancelled",
@@ -3191,11 +3318,9 @@ export class ProxySessionController {
3191
3318
  [
3192
3319
  ...(remoteHint ? [remoteHint] : []),
3193
3320
  `第 ${page + 1} 页 · 每页 ${SESSION_PAGE_SIZE} 个任务${hasNext ? "" : ` · 当前共 ${entries.length} 个任务`}`,
3194
- "点击 **New** 在对应任务的项目中创建新任务;点击 **Switch** 快速切换;点击 **Fork** 从任务最新已完成轮次创建分支;外部正在运行的任务显示 **Stop**,点击后发送 Interrupt 并变为 **Switch**。",
3195
- "点击 **NewGroup** 在对应任务的项目中创建新群和新任务;点击 **ForkGroup** 从对应任务最新已完成轮次创建分支群。",
3196
- "也可发送 **/switch [序号或任务 ID]**;不带参数切回上一个任务。外部正在执行的回合不会被接管。",
3197
- "发送 **/fork [序号或任务 ID]**,可从当前或指定任务创建分支;任务运行中时使用最近已完成轮次。",
3198
- "点击 **Status**,或发送 **/status [序号或任务 ID]**,查看当前或指定任务状态。",
3321
+ "",
3322
+ "> **Switch** 切换任务;**Stop** 停止运行;**Status** 查看状态。",
3323
+ "> **New** / **Fork** 新建 / 分支任务;**NewGroup** / **ForkGroup** 新建 / 分支群。",
3199
3324
  ],
3200
3325
  [
3201
3326
  ...(hasPrevious ? [{
@@ -3657,59 +3782,61 @@ export class ProxySessionController {
3657
3782
  else await this.outbound.sendInteractiveCard(contextKey, card);
3658
3783
  }
3659
3784
 
3785
+ private async executeHelpCommandAction(
3786
+ action: CardAction,
3787
+ contextKey: string,
3788
+ replyTarget?: MessageReplyTarget,
3789
+ ): Promise<void> {
3790
+ const commandText = typeof action.value.command === "string"
3791
+ ? action.value.command.trim()
3792
+ : "";
3793
+ if (!HELP_DEFAULT_COMMANDS.has(commandText)) {
3794
+ throw new Error("帮助卡片中的命令无效,请发送 /help 获取最新卡片。");
3795
+ }
3796
+ const command = this.router.parse(commandText);
3797
+ await this.execute(
3798
+ contextKey,
3799
+ command,
3800
+ undefined,
3801
+ replyTarget,
3802
+ undefined,
3803
+ action.userId,
3804
+ );
3805
+ }
3806
+
3660
3807
  private async help(contextKey: string): Promise<void> {
3661
- const sections: CardSection[] = [
3662
- {
3663
- lines: [
3664
- "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
3665
- "> 命令前缀示例:/sess 等同于 /sessions。",
3666
- "> 命令缩写示例:/fg 等同于 /forkgroup。",
3667
- "前缀或缩写匹配多个命令时,需要输入更长的形式。",
3668
- "**[参数]** 可选 **&#60;参数&#62;** 必填",
3669
- ],
3670
- },
3671
- {
3672
- title: "任务管理",
3673
- lines: [
3674
- "**/new [title] [--dir &#60;cwd&#62; | --nodir]** 使用默认 Agent 创建任务;--nodir 强制创建 Projectless 任务",
3675
- "**/newgroup [title] [--dir &#60;cwd&#62; | --nodir]** 创建飞书群和新任务;参数与 /new 相同",
3676
- "**/forkgroup [title]** 从当前任务最新已完成轮次创建分支并绑定到新群",
3677
- "**/fork [序号或任务 ID]** 从当前或指定任务创建分支;运行中使用最近已完成轮次",
3678
- "**/turns** 浏览当前任务的历史对话轮次,并可将对话上下文恢复到所选轮次",
3679
- "**/title &#60;新标题&#62;** 修改当前任务的标题",
3680
- "**/sessions [关键词]** 查找本机任务",
3681
- "**/switch [序号或任务 ID]** 不填参数切回上一个任务",
3682
- ],
3683
- },
3684
- {
3685
- title: "执行设置",
3686
- lines: [
3687
- "**! &#60;命令&#62;** 在当前任务目录直接执行本地命令",
3688
- "**/stop** 停止当前执行",
3689
- "**/queue &#60;prompt&#62;**(兼容 **/nosteer**) 不追加到当前轮次,按顺序排队为后续轮次",
3690
- "**/goal [目标]** 查看或创建长任务 Goal;支持 pause、resume、edit、clear",
3691
- "**/provider** 选择 AI 服务提供商;没有其他候选时仅显示当前 Provider",
3692
- "**/model** 选择当前任务使用的模型",
3693
- "**/thinking** 设置模型的思考强度",
3694
- "**/permissions** 设置执行工具前是否需要确认",
3695
- ],
3696
- },
3697
- {
3698
- title: "Agent",
3699
- lines: [
3700
- "**/agent [name]** 选择新任务使用的默认 Agent;仅配置一个时直接显示当前 Agent",
3701
- ],
3702
- },
3703
- {
3704
- title: "系统",
3705
- lines: [
3706
- "**/status [序号或任务 ID]** 查看当前或指定任务的详细状态",
3707
- "**/restart [--force]** 默认安全重启;--force 立即重启",
3708
- "**/help** 显示本帮助",
3709
- ],
3710
- },
3711
- ];
3712
- await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderSectionsCard("Agent Bot 使用帮助", sections));
3808
+ const sections: HelpCardSection[] = HELP_COMMAND_SECTIONS.map((section) => ({
3809
+ title: section.title,
3810
+ commands: section.commands.map((command) => ({
3811
+ text: command.command,
3812
+ ...(command.requiresArgument
3813
+ ? {}
3814
+ : {
3815
+ action: {
3816
+ text: command.command,
3817
+ value: {
3818
+ action: "help_command",
3819
+ command: command.command,
3820
+ contextKey,
3821
+ },
3822
+ },
3823
+ }),
3824
+ ...(command.usage ? { usage: command.usage } : {}),
3825
+ description: command.description,
3826
+ })),
3827
+ }));
3828
+ await this.outbound.sendInteractiveCard(contextKey, this.cardRenderer.renderHelpCard(
3829
+ "Agent Bot 使用帮助",
3830
+ [
3831
+ "直接发送消息即可继续当前任务;执行中发送的新消息会追加到本次任务。",
3832
+ "点击命令按钮可执行默认形式;有必填参数的命令需要手动发送。",
3833
+ "> 命令前缀示例:/sess 等同于 /sessions。",
3834
+ "> 命令缩写示例:/fg 等同于 /forkgroup。",
3835
+ "前缀或缩写匹配多个命令时,需要输入更长的形式。",
3836
+ "**[参数]** 可选 **&#60;参数&#62;** 必填",
3837
+ ],
3838
+ sections,
3839
+ ));
3713
3840
  }
3714
3841
 
3715
3842
  private ensureAgent(agentName: string) {
@@ -3,6 +3,7 @@ import { AcpSessionManager } from "../acp/AcpSessionManager.js";
3
3
  import { createId } from "../utils/id.js";
4
4
  import type {
5
5
  AgentRuntime,
6
+ AgentProcessInfo,
6
7
  ApprovalDecision,
7
8
  CreateRuntimeSessionInput,
8
9
  ModelOption,
@@ -37,6 +38,10 @@ export class AcpRuntimeAdapter implements AgentRuntime {
37
38
  return this.sessions.get(localSessionId);
38
39
  }
39
40
 
41
+ getProcessInfo(): AgentProcessInfo {
42
+ return this.acp.getProcessInfo();
43
+ }
44
+
40
45
  async createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
41
46
  const created = await this.acp.create({
42
47
  localSessionId: input.localSessionId,
@@ -2,13 +2,51 @@ export const AGENT_BOT_ENVIRONMENT = Object.freeze({
2
2
  AGENT_BOT: "1",
3
3
  });
4
4
 
5
+ export interface AgentEnvironmentContext {
6
+ profilePath?: string;
7
+ configPath?: string;
8
+ agentName?: string;
9
+ larkAppId?: string;
10
+ larkBotOpenId?: string;
11
+ larkUserOpenId?: string;
12
+ }
13
+
14
+ const RESERVED_ENVIRONMENT_PREFIXES = ["AGENT_BOT_", "FEISHU_"];
15
+
5
16
  export function agentBotEnvironment(
6
17
  baseEnv: NodeJS.ProcessEnv,
7
18
  agentEnv: Record<string, string>,
19
+ context: AgentEnvironmentContext = {},
8
20
  ): NodeJS.ProcessEnv {
9
- return {
10
- ...baseEnv,
11
- ...agentEnv,
21
+ const environment: NodeJS.ProcessEnv = {
22
+ ...withoutReservedEnvironment(baseEnv),
23
+ ...withoutReservedEnvironment(agentEnv),
12
24
  ...AGENT_BOT_ENVIRONMENT,
13
25
  };
26
+ setNonBlank(environment, "AGENT_BOT_HOME", context.profilePath);
27
+ setNonBlank(environment, "AGENT_BOT_CONFIG", context.configPath);
28
+ setNonBlank(environment, "AGENT_BOT_AGENT_NAME", context.agentName);
29
+ setNonBlank(environment, "AGENT_BOT_LARK_APP_ID", context.larkAppId);
30
+ setNonBlank(environment, "AGENT_BOT_LARK_BOT_OPEN_ID", context.larkBotOpenId);
31
+ setNonBlank(environment, "AGENT_BOT_LARK_USER_OPEN_ID", context.larkUserOpenId);
32
+ return environment;
33
+ }
34
+
35
+ function withoutReservedEnvironment(
36
+ environment: NodeJS.ProcessEnv | Record<string, string>,
37
+ ): NodeJS.ProcessEnv {
38
+ return Object.fromEntries(
39
+ Object.entries(environment).filter(([name, value]) =>
40
+ value !== undefined && !isReservedEnvironmentName(name)),
41
+ );
42
+ }
43
+
44
+ function isReservedEnvironmentName(name: string): boolean {
45
+ const normalized = name.toUpperCase();
46
+ return normalized === "AGENT_BOT"
47
+ || RESERVED_ENVIRONMENT_PREFIXES.some((prefix) => normalized.startsWith(prefix));
48
+ }
49
+
50
+ function setNonBlank(environment: NodeJS.ProcessEnv, name: string, value: string | undefined): void {
51
+ if (value?.trim()) environment[name] = value;
14
52
  }
@@ -5,19 +5,34 @@ import { CodexProcessManager } from "../codex/CodexProcessManager.js";
5
5
  import { CodexRuntime } from "../codex/CodexRuntime.js";
6
6
  import type { AppConfig } from "../config/schema.js";
7
7
  import { AcpRuntimeAdapter } from "./AcpRuntimeAdapter.js";
8
+ import type { AgentEnvironmentContext } from "./agentEnvironment.js";
8
9
  import { AgentRuntimeRegistry } from "./AgentRuntimeRegistry.js";
9
10
  import type { AgentRuntime } from "./types.js";
10
11
 
11
- export function createAgentRuntimeRegistry(config: AppConfig, logger: Logger): AgentRuntimeRegistry {
12
+ export function createAgentRuntimeRegistry(
13
+ config: AppConfig,
14
+ logger: Logger,
15
+ environmentContext: AgentEnvironmentContext = {},
16
+ ): AgentRuntimeRegistry {
12
17
  const runtimes: Record<string, AgentRuntime> = {};
13
18
  for (const [agentName, agent] of Object.entries(config.agents)) {
14
19
  const runtimeLogger = logger.child({ agentName, runtimeKind: agent.kind });
20
+ const runtimeEnvironmentContext = (): AgentEnvironmentContext => ({
21
+ ...environmentContext,
22
+ agentName,
23
+ });
15
24
  if (agent.kind === "app-server") {
16
- const processManager = new CodexProcessManager(agent.command, agent.args, agent.env, runtimeLogger);
25
+ const processManager = new CodexProcessManager(
26
+ agent.command,
27
+ agent.args,
28
+ agent.env,
29
+ runtimeLogger,
30
+ runtimeEnvironmentContext,
31
+ );
17
32
  runtimes[agentName] = new CodexRuntime(processManager, runtimeLogger);
18
33
  continue;
19
34
  }
20
- const processManager = new AcpProcessManager(runtimeLogger);
35
+ const processManager = new AcpProcessManager(runtimeLogger, runtimeEnvironmentContext);
21
36
  const sessionManager = new AcpSessionManager(agentName, agent, processManager, runtimeLogger);
22
37
  runtimes[agentName] = new AcpRuntimeAdapter(sessionManager);
23
38
  }
@@ -1,4 +1,8 @@
1
1
  export type RuntimeKind = "acp" | "codex";
2
+ export interface AgentProcessInfo {
3
+ pid?: number;
4
+ version?: string;
5
+ }
2
6
  export type PermissionMode = "auto" | "confirm";
3
7
  export type ApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
4
8
  export type ToolStatus = "running" | "completed" | "failed";
@@ -203,6 +207,7 @@ export interface ReasoningEffortOption {
203
207
 
204
208
  export interface AgentRuntime {
205
209
  readonly kind: RuntimeKind;
210
+ getProcessInfo?(): AgentProcessInfo;
206
211
  createSession(input: CreateRuntimeSessionInput): Promise<RuntimeSession>;
207
212
  resumeSession(input: ResumeRuntimeSessionInput): Promise<RuntimeSession>;
208
213
  forkSession?(input: ForkRuntimeSessionInput): Promise<RuntimeSession>;
@@ -658,9 +658,13 @@ export class StateStore {
658
658
 
659
659
  saveTurnParent(turnId: string, localSessionId: string, parentTurnId?: string): void {
660
660
  this.db.prepare(`
661
- INSERT OR IGNORE INTO turn_parent_links (
661
+ INSERT INTO turn_parent_links (
662
662
  turn_id, local_session_id, parent_turn_id, created_at
663
663
  ) VALUES (?, ?, ?, ?)
664
+ ON CONFLICT(turn_id) DO UPDATE SET
665
+ parent_turn_id = excluded.parent_turn_id
666
+ WHERE turn_parent_links.parent_turn_id IS NULL
667
+ AND excluded.parent_turn_id IS NOT NULL
664
668
  `).run(turnId, localSessionId, parentTurnId ?? null, new Date().toISOString());
665
669
  }
666
670
 
@@ -780,9 +784,6 @@ export class StateStore {
780
784
  `).all(localSessionId) as Array<{ turn_id: string; started_at: number }>;
781
785
  if (turns.length === 0) return;
782
786
 
783
- const existing = new Set((this.db.prepare(`
784
- SELECT turn_id FROM turn_parent_links WHERE local_session_id = ?
785
- `).all(localSessionId) as Array<{ turn_id: string }>).map((row) => row.turn_id));
786
787
  const resets = (this.db.prepare(`
787
788
  SELECT payload_json, created_at
788
789
  FROM audit_events
@@ -802,10 +803,14 @@ export class StateStore {
802
803
 
803
804
  let headTurnId: string | undefined;
804
805
  let resetIndex = 0;
805
- const insert = this.db.prepare(`
806
- INSERT OR IGNORE INTO turn_parent_links (
806
+ const upsert = this.db.prepare(`
807
+ INSERT INTO turn_parent_links (
807
808
  turn_id, local_session_id, parent_turn_id, created_at
808
809
  ) VALUES (?, ?, ?, ?)
810
+ ON CONFLICT(turn_id) DO UPDATE SET
811
+ parent_turn_id = excluded.parent_turn_id
812
+ WHERE turn_parent_links.parent_turn_id IS NULL
813
+ AND excluded.parent_turn_id IS NOT NULL
809
814
  `);
810
815
  const backfill = this.db.transaction(() => {
811
816
  for (const turn of turns) {
@@ -813,9 +818,7 @@ export class StateStore {
813
818
  headTurnId = resets[resetIndex]!.resetTurnId;
814
819
  resetIndex += 1;
815
820
  }
816
- if (!existing.has(turn.turn_id)) {
817
- insert.run(turn.turn_id, localSessionId, headTurnId ?? null, new Date().toISOString());
818
- }
821
+ upsert.run(turn.turn_id, localSessionId, headTurnId ?? null, new Date().toISOString());
819
822
  headTurnId = turn.turn_id;
820
823
  }
821
824
  });