aws-runtime-bridge 1.9.0 → 1.9.1

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 (84) hide show
  1. package/dist/adapter/AcodeSdkAdapter.d.ts +2 -0
  2. package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
  3. package/dist/adapter/AcodeSdkAdapter.js +84 -18
  4. package/dist/adapter/AcodeSdkAdapter.test.js +275 -13
  5. package/dist/adapter/acode-tool-names.d.ts +16 -0
  6. package/dist/adapter/acode-tool-names.d.ts.map +1 -0
  7. package/dist/adapter/acode-tool-names.js +37 -0
  8. package/dist/adapter/acode-tool-names.test.d.ts +2 -0
  9. package/dist/adapter/acode-tool-names.test.d.ts.map +1 -0
  10. package/dist/adapter/acode-tool-names.test.js +28 -0
  11. package/dist/adapter/types.d.ts +22 -3
  12. package/dist/adapter/types.d.ts.map +1 -1
  13. package/dist/adapter/types.js +2 -1
  14. package/dist/routes/git.d.ts +10 -0
  15. package/dist/routes/git.d.ts.map +1 -1
  16. package/dist/routes/git.js +37 -3
  17. package/dist/routes/git.test.js +47 -1
  18. package/dist/routes/sessions.d.ts +1 -1
  19. package/dist/routes/sessions.d.ts.map +1 -1
  20. package/dist/routes/sessions.js +11 -4
  21. package/dist/routes/sessions.test.js +3 -0
  22. package/dist/routes/terminal.d.ts +28 -8
  23. package/dist/routes/terminal.d.ts.map +1 -1
  24. package/dist/routes/terminal.js +260 -44
  25. package/dist/routes/terminal.stop-options.test.d.ts +2 -0
  26. package/dist/routes/terminal.stop-options.test.d.ts.map +1 -0
  27. package/dist/routes/terminal.stop-options.test.js +39 -0
  28. package/dist/routes/terminal.test.js +87 -4
  29. package/dist/services/session-output.d.ts +5 -0
  30. package/dist/services/session-output.d.ts.map +1 -1
  31. package/package/acode/dist/built-in-file-tools.d.ts +5 -1
  32. package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
  33. package/package/acode/dist/built-in-file-tools.js +14 -203
  34. package/package/acode/dist/builtins/commandArtifacts.d.ts +25 -0
  35. package/package/acode/dist/builtins/commandArtifacts.d.ts.map +1 -0
  36. package/package/acode/dist/builtins/commandArtifacts.js +77 -0
  37. package/package/acode/dist/builtins/commandOutputEvents.d.ts +21 -0
  38. package/package/acode/dist/builtins/commandOutputEvents.d.ts.map +1 -0
  39. package/package/acode/dist/builtins/commandOutputEvents.js +56 -0
  40. package/package/acode/dist/builtins/shellCommandPreparation.d.ts +15 -0
  41. package/package/acode/dist/builtins/shellCommandPreparation.d.ts.map +1 -0
  42. package/package/acode/dist/builtins/shellCommandPreparation.js +44 -0
  43. package/package/acode/dist/builtins/shellCommandResult.d.ts +30 -0
  44. package/package/acode/dist/builtins/shellCommandResult.d.ts.map +1 -0
  45. package/package/acode/dist/builtins/shellCommandResult.js +71 -0
  46. package/package/acode/dist/builtins/shellCommandRunner.d.ts +14 -0
  47. package/package/acode/dist/builtins/shellCommandRunner.d.ts.map +1 -0
  48. package/package/acode/dist/builtins/shellCommandRunner.js +114 -0
  49. package/package/acode/dist/builtins/shellOutputDecoder.d.ts +5 -0
  50. package/package/acode/dist/builtins/shellOutputDecoder.d.ts.map +1 -0
  51. package/package/acode/dist/builtins/shellOutputDecoder.js +30 -0
  52. package/package/acode/dist/mcpToolNames.d.ts +11 -0
  53. package/package/acode/dist/mcpToolNames.d.ts.map +1 -0
  54. package/package/acode/dist/mcpToolNames.js +17 -0
  55. package/package/acode/dist/runtime.d.ts +19 -0
  56. package/package/acode/dist/runtime.d.ts.map +1 -1
  57. package/package/acode/dist/runtime.js +233 -35
  58. package/package/acode/dist/types.d.ts +1 -1
  59. package/package/acode/dist/types.d.ts.map +1 -1
  60. package/package/acode/package.json +1 -1
  61. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts +3 -3
  62. package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
  63. package/package/aws-client-agent-mcp/dist/mcp-server.js +10 -7
  64. package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
  65. package/package/aws-client-agent-mcp/dist/mcp-server.test.js +47 -0
  66. package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
  67. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts +1 -36
  68. package/package/aws-client-agent-mcp/dist/mcp-tools.d.ts.map +1 -1
  69. package/package/aws-client-agent-mcp/dist/mcp-tools.js +3 -169
  70. package/package/aws-client-agent-mcp/dist/mcp-tools.js.map +1 -1
  71. package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts +2 -0
  72. package/package/aws-client-agent-mcp/dist/mcp-tools.test.d.ts.map +1 -0
  73. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js +45 -0
  74. package/package/aws-client-agent-mcp/dist/mcp-tools.test.js.map +1 -0
  75. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts +11 -0
  76. package/package/aws-client-agent-mcp/dist/memory-tools.d.ts.map +1 -0
  77. package/package/aws-client-agent-mcp/dist/memory-tools.js +144 -0
  78. package/package/aws-client-agent-mcp/dist/memory-tools.js.map +1 -0
  79. package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
  80. package/package/aws-client-agent-mcp/dist/websocket-client.js +1 -0
  81. package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
  82. package/package/aws-client-agent-mcp/dist/websocket-client.test.js +43 -3
  83. package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +1 -1
  84. package/package.json +1 -1
@@ -24,7 +24,53 @@ export const terminalRouter = Router();
24
24
  export const sdkSessions = new Map();
25
25
  const TOOL_RESULT_PREVIEW_MAX_LENGTH = 4000;
26
26
  const AI_REQUEST_BODY_MARKER = "\n\n请求体:\n";
27
+ const AI_REQUEST_HEADERS_MARKER = "\n\n请求头:\n";
27
28
  const startingAgents = new Set();
29
+ export function resolveRuntimeContextResetSupport(providerId, hasResetContext) {
30
+ if (providerId !== "acode") {
31
+ return {
32
+ supported: false,
33
+ statusCode: 400,
34
+ error: "context reset without restart is only supported by ACode",
35
+ };
36
+ }
37
+ if (!hasResetContext) {
38
+ return {
39
+ supported: false,
40
+ statusCode: 500,
41
+ error: "ACode adapter does not expose resetContext",
42
+ };
43
+ }
44
+ return { supported: true };
45
+ }
46
+ /**
47
+ * Decide whether SDK startup should wait for an external process PID.
48
+ * Main flow: in-process providers return immediately; external CLI providers keep PID probing for orphan cleanup.
49
+ */
50
+ export function shouldProbeSdkProcessPid(providerId) {
51
+ return providerId !== "acode";
52
+ }
53
+ /**
54
+ * 主流程:解析 runtime stop 请求的状态通知选项。
55
+ * 内部上下文重置可抑制 stopped 广播;普通手动停止默认继续通知调度端。
56
+ */
57
+ export function parseRuntimeStopOptions(body) {
58
+ const suppressStatus = typeof body === "object" &&
59
+ body !== null &&
60
+ "suppressStatus" in body &&
61
+ body.suppressStatus === true;
62
+ const reason = typeof body === "object" &&
63
+ body !== null &&
64
+ "reason" in body &&
65
+ typeof body.reason === "string" &&
66
+ body.reason.trim()
67
+ ? body.reason.trim()
68
+ : undefined;
69
+ return {
70
+ statusNotified: !suppressStatus,
71
+ reason,
72
+ };
73
+ }
28
74
  export function resolveStatusChangeUsage(actionInfo) {
29
75
  const usage = actionInfo?.usage;
30
76
  if (!usage)
@@ -39,10 +85,24 @@ export function resolveStatusChangeUsage(actionInfo) {
39
85
  return undefined;
40
86
  return { inputTokens, outputTokens };
41
87
  }
88
+ export function resolveStatusChangeRuntimeState(status, actionInfo) {
89
+ const normalizedStatus = String(status || "")
90
+ .trim()
91
+ .toLowerCase();
92
+ const normalizedActionType = String(actionInfo?.actionType || "")
93
+ .trim()
94
+ .toLowerCase();
95
+ if (normalizedStatus === "tool_using" && normalizedActionType === "idle") {
96
+ return actionInfo
97
+ ? { status: "waiting_input", actionInfo }
98
+ : { status: "waiting_input" };
99
+ }
100
+ return actionInfo ? { status, actionInfo } : { status };
101
+ }
42
102
  const terminalCommandStates = new Map();
43
103
  const DEFAULT_WINDOWS_TERMINAL_OUTPUT_ENCODING = "gb18030";
44
104
  const DEFAULT_TERMINAL_OUTPUT_ENCODING = "utf-8";
45
- const TERMINAL_COMMAND_OUTPUT_SPILLOVER_LINE_LIMIT = 100;
105
+ const TERMINAL_COMMAND_OUTPUT_SPILLOVER_LINE_LIMIT = 300;
46
106
  const TERMINAL_COMMAND_INACTIVITY_TIMEOUT_MS = 60_000;
47
107
  function padDatePart(value) {
48
108
  return String(value).padStart(2, "0");
@@ -167,7 +227,7 @@ function findActiveSdkSessionByAgentId(agentId) {
167
227
  }
168
228
  async function buildExistingSessionResponse(entry) {
169
229
  const adapter = adapterRegistry.get(entry.providerId || "claude-code");
170
- const status = resolveSdkSessionDisplayStatus(adapter.getSessionStatus(entry.sessionId), entry.currentRuntimeStatus);
230
+ const status = resolveSdkSessionDisplayStatus(adapter.getSessionStatus(entry.sessionId), entry.currentRuntimeStatus, entry.currentRuntimeActionType);
171
231
  return {
172
232
  sessionId: entry.sessionId,
173
233
  status,
@@ -187,9 +247,18 @@ async function buildExistingSessionResponse(entry) {
187
247
  * 解析 SDK 会话对外展示状态。
188
248
  * 主流程:优先使用 adapter 当前细粒度状态,其次使用路由内存记录的最近状态,最后才兼容旧会话回退 running。
189
249
  */
190
- export function resolveSdkSessionDisplayStatus(adapterStatus, currentRuntimeStatus) {
250
+ export function resolveSdkSessionDisplayStatus(adapterStatus, currentRuntimeStatus, currentRuntimeActionType) {
191
251
  const normalizedAdapterStatus = String(adapterStatus || "").trim();
192
252
  if (normalizedAdapterStatus) {
253
+ const normalizedRuntimeStatus = String(currentRuntimeStatus || "").trim();
254
+ const normalizedActionType = String(currentRuntimeActionType || "")
255
+ .trim()
256
+ .toLowerCase();
257
+ if (normalizedAdapterStatus.toLowerCase() === "tool_using" &&
258
+ normalizedRuntimeStatus.toLowerCase() === "waiting_input" &&
259
+ normalizedActionType === "idle") {
260
+ return normalizedRuntimeStatus;
261
+ }
193
262
  return normalizedAdapterStatus;
194
263
  }
195
264
  const normalizedRuntimeStatus = String(currentRuntimeStatus || "").trim();
@@ -575,6 +644,9 @@ export function formatSdkOutputEvent(event) {
575
644
  if (event.type === "thinking") {
576
645
  return event.data.text || undefined;
577
646
  }
647
+ if (event.type === "command_output") {
648
+ return event.data.text || undefined;
649
+ }
578
650
  if (event.type === "error") {
579
651
  return event.data.text
580
652
  ? `\r\n[SDK Error] ${event.data.text}\r\n`
@@ -587,14 +659,44 @@ export function buildSdkOutputMetadata(event) {
587
659
  return {
588
660
  sourceEventType: event.type,
589
661
  logCategory: "acode",
662
+ ...buildCommandOutputMetadata(event),
590
663
  ...(logMessage
591
664
  ? {
592
- logLevel: event.type === "error" ? "error" : "info",
665
+ logLevel: resolveSdkLogLevel(event),
593
666
  logMessage,
594
667
  }
595
668
  : {}),
596
669
  };
597
670
  }
671
+ function resolveSdkLogLevel(event) {
672
+ if (event.type === "error") {
673
+ return "error";
674
+ }
675
+ if (event.type === "command_output" && event.data.stream === "stderr") {
676
+ return "warn";
677
+ }
678
+ return "info";
679
+ }
680
+ function buildCommandOutputMetadata(event) {
681
+ if (event.type !== "command_output") {
682
+ return {};
683
+ }
684
+ return {
685
+ ...(typeof event.data.stream === "string"
686
+ ? { stream: event.data.stream }
687
+ : {}),
688
+ ...(typeof event.data.sequence === "number"
689
+ ? { sequence: event.data.sequence }
690
+ : {}),
691
+ ...(typeof event.data.cwd === "string" ? { cwd: event.data.cwd } : {}),
692
+ ...(typeof event.data.commandFilePath === "string"
693
+ ? { commandFilePath: event.data.commandFilePath }
694
+ : {}),
695
+ ...(typeof event.data.commandLineCount === "number"
696
+ ? { commandLineCount: event.data.commandLineCount }
697
+ : {}),
698
+ };
699
+ }
598
700
  function formatSdkLogMessage(event) {
599
701
  if (event.type === "ai_request") {
600
702
  const model = event.data.model || "unknown-model";
@@ -610,6 +712,9 @@ function formatSdkLogMessage(event) {
610
712
  if (event.type === "tool_use_end") {
611
713
  return formatSdkToolLogMessage(event, "完成");
612
714
  }
715
+ if (event.type === "command_output") {
716
+ return formatCommandOutputLogMessage(event);
717
+ }
613
718
  if (event.type === "turn_complete") {
614
719
  const finishReason = event.data.finishReason
615
720
  ? `, finish=${event.data.finishReason}`
@@ -624,6 +729,15 @@ function formatSdkLogMessage(event) {
624
729
  }
625
730
  return undefined;
626
731
  }
732
+ function formatCommandOutputLogMessage(event) {
733
+ const stream = typeof event.data.stream === "string" ? event.data.stream : "mixed";
734
+ const sequence = typeof event.data.sequence === "number" ? ` #${event.data.sequence}` : "";
735
+ const normalized = String(event.data.text || "")
736
+ .replace(/\s+/g, " ")
737
+ .trim();
738
+ const preview = normalized.length > 240 ? `${normalized.slice(0, 240)}...` : normalized;
739
+ return `命令输出(${stream}${sequence}): ${preview || "(empty)"}`;
740
+ }
627
741
  function formatSdkToolLogMessage(event, phaseLabel) {
628
742
  const actionInfo = getToolActionInfo(event.data.toolName, event.data.toolInput);
629
743
  const actionLabel = event.data.actionLabel || actionInfo.actionLabel || "调用工具";
@@ -694,7 +808,10 @@ function displayStringList(value) {
694
808
  function formatAiRequestDetail(data) {
695
809
  const lines = [
696
810
  data.model ? `模型: ${data.model}` : undefined,
697
- data.endpoint ? `接口: ${data.endpoint}` : undefined,
811
+ data.endpoint || data.requestUrl
812
+ ? `接口: ${data.endpoint || data.requestUrl}`
813
+ : undefined,
814
+ data.requestMethod ? `方法: ${data.requestMethod}` : undefined,
698
815
  typeof data.stream === "boolean"
699
816
  ? `流式: ${data.stream ? "是" : "否"}`
700
817
  : undefined,
@@ -708,10 +825,22 @@ function formatAiRequestDetail(data) {
708
825
  ? `最后请求: ${data.lastUserMessagePreview}`
709
826
  : undefined,
710
827
  ].filter((line) => Boolean(line));
828
+ const requestHeaderLines = formatAiRequestHeaders(data.requestHeaders);
711
829
  const base = lines.join("\n") || "ACode 正在发送 AI 请求";
712
- return data.requestBody
713
- ? `${base}${AI_REQUEST_BODY_MARKER}${data.requestBody}`
714
- : base;
830
+ const headerBlock = requestHeaderLines.length > 0
831
+ ? `${AI_REQUEST_HEADERS_MARKER}${requestHeaderLines.join("\n")}`
832
+ : "";
833
+ const bodyBlock = data.requestBody
834
+ ? `${AI_REQUEST_BODY_MARKER}${data.requestBody}`
835
+ : "";
836
+ return `${base}${headerBlock}${bodyBlock}`;
837
+ }
838
+ function formatAiRequestHeaders(headers) {
839
+ if (!headers)
840
+ return [];
841
+ return Object.entries(headers)
842
+ .map(([name, value]) => `${name}: ${value}`)
843
+ .filter((line) => line.trim().length > 0);
715
844
  }
716
845
  /**
717
846
  * 主干流程:把 provider 事件转换为面板运行状态。
@@ -719,7 +848,19 @@ function formatAiRequestDetail(data) {
719
848
  * text_delta 表示助手正在生成;thinking 表示推理/内部过程。
720
849
  * tool_use_start 表示正在执行工具;turn_complete 才表示真正空闲。
721
850
  */
722
- export function resolveRuntimeStatusFromProviderEvent(event) {
851
+ function isWaitingInputProgressEvent(event, currentRuntimeStatus) {
852
+ const normalizedStatus = String(currentRuntimeStatus || "")
853
+ .trim()
854
+ .toLowerCase();
855
+ if (normalizedStatus !== "waiting_input") {
856
+ return false;
857
+ }
858
+ return event.type === "thinking" || event.type === "text_delta";
859
+ }
860
+ export function resolveRuntimeStatusFromProviderEvent(event, currentRuntimeStatus) {
861
+ if (isWaitingInputProgressEvent(event, currentRuntimeStatus)) {
862
+ return undefined;
863
+ }
723
864
  if (event.type === "ai_request") {
724
865
  return {
725
866
  status: "thinking",
@@ -812,7 +953,7 @@ function wireSdkAdapterEvents(adapter, definition) {
812
953
  const entry = sdkSessions.get(event.sessionId);
813
954
  if (entry) {
814
955
  console.log(`[${definition.displayName} Adapter] Event: ${event.type} for session ${event.sessionId}`);
815
- const runtimeStatus = resolveRuntimeStatusFromProviderEvent(event);
956
+ const runtimeStatus = resolveRuntimeStatusFromProviderEvent(event, entry.currentRuntimeStatus);
816
957
  if (runtimeStatus) {
817
958
  void sendStatus(entry.agentId, event.sessionId, runtimeStatus.status, runtimeStatus.actionInfo, resolveStatusChangeUsage(runtimeStatus.actionInfo), {
818
959
  runtimeAccessToken: entry.runtimeAccessToken,
@@ -832,18 +973,20 @@ function wireSdkAdapterEvents(adapter, definition) {
832
973
  adapter.on("status-change", (sessionId, status, actionInfo) => {
833
974
  const entry = sdkSessions.get(sessionId);
834
975
  if (entry) {
835
- entry.currentRuntimeStatus = status;
836
- entry.currentRuntimeActionType = actionInfo?.actionType;
837
- entry.currentRuntimeActionLabel = actionInfo?.actionLabel;
838
- entry.currentRuntimeActionDetail = actionInfo?.actionDetail;
839
- console.log(`[${definition.displayName} Adapter] Status changed: ${status} for agent ${entry.agentId}`, actionInfo || "");
976
+ const runtimeState = resolveStatusChangeRuntimeState(status, actionInfo);
977
+ entry.currentRuntimeStatus = runtimeState.status;
978
+ entry.currentRuntimeActionType = runtimeState.actionInfo?.actionType;
979
+ entry.currentRuntimeActionLabel = runtimeState.actionInfo?.actionLabel;
980
+ entry.currentRuntimeActionDetail =
981
+ runtimeState.actionInfo?.actionDetail;
982
+ console.log(`[${definition.displayName} Adapter] Status changed: ${runtimeState.status} for agent ${entry.agentId}`, runtimeState.actionInfo || "");
840
983
  void updatePersistedSessionRuntimeState(sessionId, {
841
- runtimeStatus: status,
842
- runtimeActionType: actionInfo?.actionType,
843
- runtimeActionLabel: actionInfo?.actionLabel,
844
- runtimeActionDetail: actionInfo?.actionDetail,
984
+ runtimeStatus: runtimeState.status,
985
+ runtimeActionType: runtimeState.actionInfo?.actionType,
986
+ runtimeActionLabel: runtimeState.actionInfo?.actionLabel,
987
+ runtimeActionDetail: runtimeState.actionInfo?.actionDetail,
845
988
  });
846
- void sendStatus(entry.agentId, sessionId, status, actionInfo, resolveStatusChangeUsage(actionInfo), {
989
+ void sendStatus(entry.agentId, sessionId, runtimeState.status, runtimeState.actionInfo, resolveStatusChangeUsage(runtimeState.actionInfo), {
847
990
  runtimeAccessToken: entry.runtimeAccessToken,
848
991
  schedulerBaseUrl: entry.schedulerBaseUrl,
849
992
  });
@@ -992,29 +1135,31 @@ async function startSdkSession(req, res) {
992
1135
  // ★ 尝试获取 SDK 进程的 PID(用于孤儿进程检测)
993
1136
  // SDK 不直接暴露 PID,通过进程检测服务查找匹配的进程
994
1137
  let sdkPid;
995
- try {
996
- // 等待一小段时间让进程启动
997
- await new Promise((resolve) => setTimeout(resolve, 500));
998
- // 尝试通过 adapter 获取 PID(如果支持)
999
- sdkPid = adapter.getSessionPid?.(sessionId);
1000
- // 如果 adapter 不支持,尝试通过进程检测服务查找
1001
- if (!sdkPid) {
1002
- const processes = findClaudeCodeProcesses();
1003
- // 查找匹配 agentId 或工作目录的进程
1004
- const matched = processes.find((p) => p.agentId === agentId ||
1005
- p.cwd === workspacePath ||
1006
- p.command.includes(workspacePath));
1007
- if (matched) {
1008
- sdkPid = matched.pid;
1009
- console.log(`[Runtime] Found SDK process PID: ${sdkPid} for agentId=${agentId}`);
1138
+ if (shouldProbeSdkProcessPid(providerId)) {
1139
+ try {
1140
+ // 等待一小段时间让进程启动
1141
+ await new Promise((resolve) => setTimeout(resolve, 500));
1142
+ // 尝试通过 adapter 获取 PID(如果支持)
1143
+ sdkPid = adapter.getSessionPid?.(sessionId);
1144
+ // 如果 adapter 不支持,尝试通过进程检测服务查找
1145
+ if (!sdkPid) {
1146
+ const processes = findClaudeCodeProcesses();
1147
+ // 查找匹配 agentId 或工作目录的进程
1148
+ const matched = processes.find((p) => p.agentId === agentId ||
1149
+ p.cwd === workspacePath ||
1150
+ p.command.includes(workspacePath));
1151
+ if (matched) {
1152
+ sdkPid = matched.pid;
1153
+ console.log(`[Runtime] Found SDK process PID: ${sdkPid} for agentId=${agentId}`);
1154
+ }
1010
1155
  }
1011
1156
  }
1012
- }
1013
- catch (pidError) {
1014
- console.warn(`[Runtime] Failed to get SDK PID for agentId=${agentId}:`, pidError);
1157
+ catch (pidError) {
1158
+ console.warn(`[Runtime] Failed to get SDK PID for agentId=${agentId}:`, pidError);
1159
+ }
1015
1160
  }
1016
1161
  // ★ 持久化会话信息(包含 PID)
1017
- const startedStatus = resolveSdkSessionDisplayStatus(adapter.getSessionStatus(sessionId), sdkSessions.get(sessionId)?.currentRuntimeStatus);
1162
+ const startedStatus = resolveSdkSessionDisplayStatus(adapter.getSessionStatus(sessionId), sdkSessions.get(sessionId)?.currentRuntimeStatus, sdkSessions.get(sessionId)?.currentRuntimeActionType);
1018
1163
  await upsertPersistedSession({
1019
1164
  sessionId,
1020
1165
  agentId,
@@ -1369,12 +1514,65 @@ terminalRouter.post("/resize", validateToken, (req, res) => {
1369
1514
  rows: Number(rows),
1370
1515
  });
1371
1516
  });
1517
+ terminalRouter.post("/reset-context", validateToken, async (req, res) => {
1518
+ const { sessionId, reason } = req.body || {};
1519
+ if (!sessionId) {
1520
+ res.status(400).json({ error: "sessionId is required" });
1521
+ return;
1522
+ }
1523
+ const sdkEntry = sdkSessions.get(sessionId);
1524
+ if (!sdkEntry) {
1525
+ res.status(404).json({ error: "SDK session not found" });
1526
+ return;
1527
+ }
1528
+ const providerId = sdkEntry.providerId || "claude-code";
1529
+ const adapter = adapterRegistry.get(providerId);
1530
+ const resetContext = adapter.resetContext?.bind(adapter);
1531
+ const support = resolveRuntimeContextResetSupport(providerId, Boolean(resetContext));
1532
+ if (!support.supported) {
1533
+ res.status(support.statusCode).json({ error: support.error });
1534
+ return;
1535
+ }
1536
+ if (!resetContext) {
1537
+ res
1538
+ .status(500)
1539
+ .json({ error: "ACode adapter does not expose resetContext" });
1540
+ return;
1541
+ }
1542
+ try {
1543
+ await resetContext(String(sessionId));
1544
+ const status = resolveSdkSessionDisplayStatus(adapter.getSessionStatus(String(sessionId)), sdkEntry.currentRuntimeStatus, sdkEntry.currentRuntimeActionType);
1545
+ res.json({
1546
+ ok: true,
1547
+ contextReset: true,
1548
+ sessionId,
1549
+ status,
1550
+ runtimeStatus: status,
1551
+ agentId: sdkEntry.agentId,
1552
+ mode: "sdk",
1553
+ providerId,
1554
+ reason: typeof reason === "string" && reason.trim()
1555
+ ? reason.trim()
1556
+ : undefined,
1557
+ });
1558
+ }
1559
+ catch (error) {
1560
+ const errorMessage = error instanceof Error ? error.message : String(error);
1561
+ res.status(500).json({
1562
+ ok: false,
1563
+ contextReset: false,
1564
+ mode: "sdk",
1565
+ error: errorMessage,
1566
+ });
1567
+ }
1568
+ });
1372
1569
  /**
1373
1570
  * 停止 SDK 会话。
1374
1571
  * POST /runtime/stop
1375
1572
  */
1376
1573
  terminalRouter.post("/stop", validateToken, async (req, res) => {
1377
1574
  const { sessionId } = req.body || {};
1575
+ const stopOptions = parseRuntimeStopOptions(req.body);
1378
1576
  if (!sessionId) {
1379
1577
  res.status(400).json({ error: "sessionId is required" });
1380
1578
  return;
@@ -1382,11 +1580,18 @@ terminalRouter.post("/stop", validateToken, async (req, res) => {
1382
1580
  const sdkEntry = sdkSessions.get(sessionId);
1383
1581
  if (!sdkEntry) {
1384
1582
  await removePersistedSession(sessionId);
1385
- res.json({ ok: true, status: "stopped", mode: "none" });
1583
+ res.json({
1584
+ ok: true,
1585
+ status: "stopped",
1586
+ mode: "none",
1587
+ statusNotified: false,
1588
+ reason: stopOptions.reason,
1589
+ });
1386
1590
  return;
1387
1591
  }
1388
1592
  const agentId = sdkEntry.agentId;
1389
1593
  try {
1594
+ console.log(`[Runtime] Stopping SDK session ${sessionId}; suppressStatus=${!stopOptions.statusNotified}, reason=${stopOptions.reason || "unspecified"}`);
1390
1595
  const providerId = sdkEntry.providerId || "claude-code";
1391
1596
  const adapter = adapterRegistry.get(providerId);
1392
1597
  await adapter.terminateSession(sessionId);
@@ -1395,11 +1600,22 @@ terminalRouter.post("/stop", validateToken, async (req, res) => {
1395
1600
  await removePersistedSession(sessionId);
1396
1601
  const processManager = getAgentProcessManager();
1397
1602
  await processManager.removeProcess(agentId);
1398
- await sendStatus(agentId, null, "stopped", undefined, undefined, {
1399
- runtimeAccessToken: sdkEntry.runtimeAccessToken,
1400
- schedulerBaseUrl: sdkEntry.schedulerBaseUrl,
1603
+ if (stopOptions.statusNotified) {
1604
+ await sendStatus(agentId, null, "stopped", undefined, undefined, {
1605
+ runtimeAccessToken: sdkEntry.runtimeAccessToken,
1606
+ schedulerBaseUrl: sdkEntry.schedulerBaseUrl,
1607
+ });
1608
+ }
1609
+ else {
1610
+ console.log(`[Runtime] Stopped status notification suppressed for SDK session ${sessionId}; reason=${stopOptions.reason || "unspecified"}`);
1611
+ }
1612
+ res.json({
1613
+ ok: true,
1614
+ status: "stopped",
1615
+ mode: "sdk",
1616
+ statusNotified: stopOptions.statusNotified,
1617
+ reason: stopOptions.reason,
1401
1618
  });
1402
- res.json({ ok: true, status: "stopped", mode: "sdk" });
1403
1619
  }
1404
1620
  catch (error) {
1405
1621
  const errorMessage = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=terminal.stop-options.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminal.stop-options.test.d.ts","sourceRoot":"","sources":["../../src/routes/terminal.stop-options.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { parseRuntimeStopOptions, resolveRuntimeContextResetSupport, } from "./terminal.js";
3
+ describe("runtime stop options", () => {
4
+ it("notifies stopped status for manual stop requests by default", () => {
5
+ const options = parseRuntimeStopOptions({ sessionId: "session-1" });
6
+ expect(options).toEqual({
7
+ statusNotified: true,
8
+ reason: undefined,
9
+ });
10
+ });
11
+ it("suppresses stopped status for internal context reset requests", () => {
12
+ const options = parseRuntimeStopOptions({
13
+ sessionId: "session-1",
14
+ suppressStatus: true,
15
+ reason: "context_reset_after_task_submit",
16
+ });
17
+ expect(options).toEqual({
18
+ statusNotified: false,
19
+ reason: "context_reset_after_task_submit",
20
+ });
21
+ });
22
+ });
23
+ describe("runtime context reset support", () => {
24
+ it("allows only ACode sessions whose adapter exposes resetContext", () => {
25
+ expect(resolveRuntimeContextResetSupport("acode", true)).toEqual({
26
+ supported: true,
27
+ });
28
+ expect(resolveRuntimeContextResetSupport("claude-code", false)).toEqual({
29
+ supported: false,
30
+ statusCode: 400,
31
+ error: "context reset without restart is only supported by ACode",
32
+ });
33
+ expect(resolveRuntimeContextResetSupport("acode", false)).toEqual({
34
+ supported: false,
35
+ statusCode: 500,
36
+ error: "ACode adapter does not expose resetContext",
37
+ });
38
+ });
39
+ });
@@ -3,7 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { describe, expect, it } from "vitest";
5
5
  import { SDK_PROVIDER_DEFINITIONS } from "../adapter/SdkProviderSpi.js";
6
- import { buildAcodeCommandOutputPath, buildClaudeCodeLaunchConfig, buildRuntimeEnv, buildSdkOutputMetadata, buildToolResultTimelineActionInfo, countTerminalOutputLines, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatCommandInactivityTimeoutNotice, formatCommandOutputSpilloverNotice, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveSdkInputMessage, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveSdkSessionDisplayStatus, resolveRuntimeStatusFromProviderEvent, resolveStatusChangeUsage, } from "./terminal.js";
6
+ import { buildAcodeCommandOutputPath, buildClaudeCodeLaunchConfig, buildRuntimeEnv, buildSdkOutputMetadata, buildToolResultTimelineActionInfo, countTerminalOutputLines, createTerminalOutputDecoder, decodeTerminalOutputChunk, evaluatePersistedSessionReuse, formatCommandInactivityTimeoutNotice, formatCommandOutputSpilloverNotice, formatTerminalPrompt, formatSdkOutputEvent, normalizeTerminalCommandInput, parseTerminalDirectoryChangeTarget, resolveSdkInputMessage, resolveTerminalOutputEncoding, resolveSdkProviderId, resolveSdkSessionDisplayStatus, resolveRuntimeStatusFromProviderEvent, resolveStatusChangeRuntimeState, resolveStatusChangeUsage, shouldProbeSdkProcessPid, } from "./terminal.js";
7
7
  describe("terminal route validation", () => {
8
8
  it("requires agentId and workspacePath for start", () => {
9
9
  const validateStartRequest = (body) => {
@@ -90,6 +90,10 @@ describe("terminal route validation", () => {
90
90
  });
91
91
  });
92
92
  describe("SDK startup idle command ordering", () => {
93
+ it("skips PID probing for in-process ACode sessions", () => {
94
+ expect(shouldProbeSdkProcessPid("acode")).toBe(false);
95
+ expect(shouldProbeSdkProcessPid("claude-code")).toBe(true);
96
+ });
93
97
  it("sets idle commands immediately after adapter startup and before PID probing or persistence", async () => {
94
98
  const calls = [];
95
99
  const adapter = {
@@ -217,10 +221,19 @@ describe("SDK provider event runtime status mapping", () => {
217
221
  expect(resolveRuntimeStatusFromProviderEvent(event("thinking"))?.status).toBe("thinking");
218
222
  expect(resolveRuntimeStatusFromProviderEvent(event("text_delta"))?.status).toBe("thinking");
219
223
  });
224
+ it("keeps command output chunks from changing the runtime status", () => {
225
+ expect(resolveRuntimeStatusFromProviderEvent(event("command_output", { text: "stdout chunk", stream: "stdout" }))).toBeUndefined();
226
+ });
220
227
  it("maps AI request events to thinking status with request details", () => {
221
228
  const state = resolveRuntimeStatusFromProviderEvent(event("ai_request", {
222
229
  model: "test-model",
223
230
  endpoint: "https://example.invalid/v1/chat/completions",
231
+ requestMethod: "POST",
232
+ requestUrl: "https://example.invalid/v1/chat/completions",
233
+ requestHeaders: {
234
+ "Content-Type": "application/json",
235
+ Authorization: "Bearer ***",
236
+ },
224
237
  stream: true,
225
238
  messageCount: 3,
226
239
  toolCount: 2,
@@ -234,9 +247,25 @@ describe("SDK provider event runtime status mapping", () => {
234
247
  });
235
248
  expect(state?.actionInfo?.actionDetail).toContain("模型: test-model");
236
249
  expect(state?.actionInfo?.actionDetail).toContain("接口: https://example.invalid/v1/chat/completions");
250
+ expect(state?.actionInfo?.actionDetail).toContain("方法: POST");
251
+ expect(state?.actionInfo?.actionDetail).toContain("请求头:\nContent-Type: application/json\nAuthorization: Bearer ***");
237
252
  expect(state?.actionInfo?.actionDetail).toContain("最后请求: inspect status");
238
253
  expect(state?.actionInfo?.actionDetail).toContain('请求体:\n{"model":"test-model"}');
239
254
  });
255
+ it("does not let idle polling text chunks override waiting_input", () => {
256
+ expect(resolveRuntimeStatusFromProviderEvent(event("thinking"), "waiting_input")).toBeUndefined();
257
+ expect(resolveRuntimeStatusFromProviderEvent(event("text_delta"), "waiting_input")).toBeUndefined();
258
+ });
259
+ it("still reports AI request details when a request starts from waiting_input", () => {
260
+ expect(resolveRuntimeStatusFromProviderEvent(event("ai_request", {
261
+ model: "test-model",
262
+ requestBody: '{"model":"test-model"}',
263
+ }), "waiting_input")?.actionInfo).toMatchObject({
264
+ actionType: "ai_request",
265
+ actionLabel: "AI请求",
266
+ actionDetail: expect.stringContaining('请求体:\n{"model":"test-model"}'),
267
+ });
268
+ });
240
269
  it("maps tool start events to tool_using with action info", () => {
241
270
  const state = resolveRuntimeStatusFromProviderEvent(event("tool_use_start", {
242
271
  toolName: "mcp__aws__my_task",
@@ -315,8 +344,8 @@ describe("terminal command helpers", () => {
315
344
  expect(outputPath).toBe(path.join("D:", "code", "repo", ".agentswork", "acode", "cmd-dist", "2026-02-03", "04", "05", "20260203-040506-007-cmd.out"));
316
345
  });
317
346
  it("formats ACode long-output and inactivity notices for terminal users", () => {
318
- expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 100)).toContain("命令输出超过 100 行");
319
- expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 100)).toContain("/repo/out.txt");
347
+ expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 300)).toContain("命令输出超过 300 行");
348
+ expect(formatCommandOutputSpilloverNotice("/repo/out.txt", 300)).toContain("/repo/out.txt");
320
349
  expect(formatCommandInactivityTimeoutNotice(60_000)).toContain("超过 60 秒没有新的输出");
321
350
  });
322
351
  });
@@ -343,6 +372,20 @@ describe("resolveSdkProviderId", () => {
343
372
  });
344
373
  });
345
374
  describe("SDK status usage forwarding", () => {
375
+ it("normalizes idle tool status changes to waiting_input", () => {
376
+ expect(resolveStatusChangeRuntimeState("tool_using", {
377
+ actionType: "idle",
378
+ actionLabel: "监听消息",
379
+ actionDetail: "aws-mcp/poll_message",
380
+ })).toEqual({
381
+ status: "waiting_input",
382
+ actionInfo: {
383
+ actionType: "idle",
384
+ actionLabel: "监听消息",
385
+ actionDetail: "aws-mcp/poll_message",
386
+ },
387
+ });
388
+ });
346
389
  it("keeps accumulated usage from status-change action info", () => {
347
390
  expect(resolveStatusChangeUsage({
348
391
  usage: { inputTokens: 12000, outputTokens: 345 },
@@ -435,6 +478,20 @@ describe("SDK output forwarding", () => {
435
478
  };
436
479
  expect(formatSdkOutputEvent(thinkingEvent)).toBe("internal prompt or reasoning");
437
480
  });
481
+ it("formats command output provider events as realtime output chunks", () => {
482
+ const commandOutputEvent = {
483
+ type: "command_output",
484
+ sessionId: "session-1",
485
+ timestamp: new Date(0).toISOString(),
486
+ data: {
487
+ text: "stdout chunk",
488
+ stream: "stdout",
489
+ sequence: 7,
490
+ cwd: "/repo",
491
+ },
492
+ };
493
+ expect(formatSdkOutputEvent(commandOutputEvent)).toBe("stdout chunk");
494
+ });
438
495
  it("formats SDK errors as terminal output and ignores non-output events", () => {
439
496
  const errorEvent = {
440
497
  type: "error",
@@ -468,7 +525,11 @@ describe("SDK output forwarding", () => {
468
525
  type: "ai_request",
469
526
  sessionId: "session-1",
470
527
  timestamp: new Date(0).toISOString(),
471
- data: { model: "test-model", endpoint: "openai-compatible", toolCount: 2 },
528
+ data: {
529
+ model: "test-model",
530
+ endpoint: "openai-compatible",
531
+ toolCount: 2,
532
+ },
472
533
  };
473
534
  const errorEvent = {
474
535
  type: "error",
@@ -489,6 +550,28 @@ describe("SDK output forwarding", () => {
489
550
  logMessage: "provider failed",
490
551
  });
491
552
  });
553
+ it("builds ordered ACode log metadata for command output callbacks", () => {
554
+ const commandOutputEvent = {
555
+ type: "command_output",
556
+ sessionId: "session-1",
557
+ timestamp: new Date(0).toISOString(),
558
+ data: {
559
+ text: "stdout chunk",
560
+ stream: "stdout",
561
+ sequence: 7,
562
+ cwd: "/repo",
563
+ },
564
+ };
565
+ expect(buildSdkOutputMetadata(commandOutputEvent)).toEqual({
566
+ sourceEventType: "command_output",
567
+ logCategory: "acode",
568
+ logLevel: "info",
569
+ logMessage: "命令输出(stdout #7): stdout chunk",
570
+ stream: "stdout",
571
+ sequence: 7,
572
+ cwd: "/repo",
573
+ });
574
+ });
492
575
  it("does not mark streaming text chunks as standalone ACode log rows", () => {
493
576
  const metadata = buildSdkOutputMetadata({
494
577
  type: "text_delta",
@@ -21,6 +21,11 @@ export interface RuntimeOutputMetadata {
21
21
  logLevel?: "info" | "warn" | "error";
22
22
  logMessage?: string;
23
23
  logCategory?: string;
24
+ stream?: string;
25
+ sequence?: number;
26
+ cwd?: string;
27
+ commandFilePath?: string;
28
+ commandLineCount?: number;
24
29
  }
25
30
  /** 活跃会话存储 */
26
31
  export declare const sessions: Map<string, Session>;