evolcore 0.0.2 → 0.0.4

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 (206) hide show
  1. package/CHANGELOG.md +93 -778
  2. package/README.md +45 -10
  3. package/bin/ec-safe-output.js +89 -24
  4. package/dist/agents/baseagent.js +46 -0
  5. package/dist/agents/claude-runner.js +198 -48
  6. package/dist/agents/codex-app-server-client.js +68 -0
  7. package/dist/agents/codex-runner.js +203 -8
  8. package/dist/agents/runner-types.js +2 -2
  9. package/dist/aun/aid/agentmd.js +79 -0
  10. package/dist/aun/aid/encryption-seed-policy.js +29 -0
  11. package/dist/aun/aid/index.js +1 -1
  12. package/dist/aun/aid/store.js +2 -5
  13. package/dist/aun/outbox.js +8 -0
  14. package/dist/channels/aun.js +241 -116
  15. package/dist/channels/daemon.js +18 -4
  16. package/dist/channels/dingtalk.js +1005 -260
  17. package/dist/channels/feishu.js +184 -56
  18. package/dist/channels/qqbot.js +23 -1
  19. package/dist/channels/wechat.js +303 -155
  20. package/dist/channels/wecom.js +1132 -164
  21. package/dist/cli/agent-command.js +2 -1
  22. package/dist/cli/aun-commands.js +98 -43
  23. package/dist/cli/bench.js +2 -2
  24. package/dist/cli/contact.js +71 -0
  25. package/dist/cli/ctl-command.js +2 -2
  26. package/dist/cli/daemon-commands.js +77 -214
  27. package/dist/cli/handoff-command.js +4 -3
  28. package/dist/cli/help.js +9 -5
  29. package/dist/cli/index.js +132 -116
  30. package/dist/cli/init-channel.js +242 -129
  31. package/dist/cli/init.js +70 -31
  32. package/dist/cli/model.js +2 -1
  33. package/dist/cli/net-check.js +2 -2
  34. package/dist/cli/queue-command.js +30 -6
  35. package/dist/cli/raw-key-input.js +25 -0
  36. package/dist/cli/response.js +5 -6
  37. package/dist/cli/restart-monitor.js +25 -1
  38. package/dist/cli/stats.js +6 -4
  39. package/dist/cli/trigger-command.js +146 -25
  40. package/dist/cli/version.js +6 -1
  41. package/dist/config/access-policy-domain.js +38 -0
  42. package/dist/config/access-policy.js +134 -0
  43. package/dist/config/builtin-role-templates.js +42 -17
  44. package/dist/config/builtin-roles.js +31 -6
  45. package/dist/config/config-field-policy.js +17 -5
  46. package/dist/config/config-manager.js +409 -29
  47. package/dist/config/config-operation-service.js +35 -38
  48. package/dist/config/contact-alias.js +68 -0
  49. package/dist/config/contact-bind-code.js +274 -0
  50. package/dist/config/contact-book-store.js +622 -0
  51. package/dist/config/contact-book-v2-startup.js +35 -0
  52. package/dist/config/contact-book.js +185 -300
  53. package/dist/config/contact-operation-service.js +116 -0
  54. package/dist/config/contact-request-service.js +331 -0
  55. package/dist/config/peer-role-resolver.js +136 -55
  56. package/dist/config/role-ranks.js +18 -0
  57. package/dist/config/role-service.js +16 -19
  58. package/dist/config/role-store.js +16 -5
  59. package/dist/config/roles.js +10 -1
  60. package/dist/config/schema-registry.js +49 -24
  61. package/dist/config-store.js +14 -4
  62. package/dist/core/auth/agent-delegation.js +105 -11
  63. package/dist/core/auth/authorization-audit.js +16 -1
  64. package/dist/core/auth/operation-authorizer.js +10 -0
  65. package/dist/core/auth/operation-catalog.js +105 -1
  66. package/dist/core/auth/trigger-authorization.js +15 -0
  67. package/dist/core/bootstrap-service.js +2 -9
  68. package/dist/core/channel-loader.js +6 -2
  69. package/dist/core/command/command-handler.js +113 -21
  70. package/dist/core/command/connect-menu.js +599 -0
  71. package/dist/core/command/evol-menu-version-gate.js +38 -0
  72. package/dist/core/command/group-menu.js +421 -0
  73. package/dist/core/command/menu-handler.js +382 -100
  74. package/dist/core/command/menu-protocol.js +8 -2
  75. package/dist/core/command/menu-token-store.js +102 -0
  76. package/dist/core/command/role-menu.js +128 -29
  77. package/dist/core/command/slash-gate.js +1 -1
  78. package/dist/core/command/slash-handler.js +186 -49
  79. package/dist/core/daemon-file-cache.js +40 -6
  80. package/dist/core/event-catalog.js +99 -0
  81. package/dist/core/evolagent-registry.js +4 -40
  82. package/dist/core/evolagent.js +20 -9
  83. package/dist/core/handoff/runtime.js +162 -37
  84. package/dist/core/handoff/store.js +46 -2
  85. package/dist/core/handoff/types.js +1 -0
  86. package/dist/core/inference/text-inference.js +16 -74
  87. package/dist/core/message/file-markers.js +7 -0
  88. package/dist/core/message/im-renderer.js +20 -14
  89. package/dist/core/message/inbound-admission.js +124 -0
  90. package/dist/core/message/message-bridge.js +670 -57
  91. package/dist/core/message/message-log.js +1 -0
  92. package/dist/core/message/message-queue.js +195 -10
  93. package/dist/core/message/message-utils.js +8 -2
  94. package/dist/core/message/peer-mode.js +7 -8
  95. package/dist/core/message/response-engine.js +642 -96
  96. package/dist/core/message/send-receipt.js +24 -0
  97. package/dist/core/message/stream-debouncer.js +11 -2
  98. package/dist/core/permission/approval-gateway.js +17 -10
  99. package/dist/core/permission/ec-command-parser.js +101 -46
  100. package/dist/core/permission/tool-policy.js +115 -38
  101. package/dist/core/protected-paths.js +38 -11
  102. package/dist/core/session/session-fs-store.js +44 -3
  103. package/dist/core/session/session-manager.js +86 -8
  104. package/dist/core/session/session-mapper.js +2 -0
  105. package/dist/core/session/session-renew.js +125 -69
  106. package/dist/core/session/session-turn-coordinator.js +5 -1
  107. package/dist/eck/kit-renderer.js +12 -1
  108. package/dist/eck/message-renderer.js +79 -1
  109. package/dist/index.js +285 -94
  110. package/dist/ipc.js +114 -8
  111. package/dist/paths.js +3 -0
  112. package/dist/response-system/config-resolver.js +29 -0
  113. package/dist/response-system/coordinator.js +8 -32
  114. package/dist/response-system/engines/v1/proactive-flow.js +1 -1
  115. package/dist/response-system/index.js +1 -0
  116. package/dist/response-system/modes/single-session/index.js +7 -4
  117. package/dist/stats/billing.js +20 -8
  118. package/dist/trigger/manager.js +3 -0
  119. package/dist/trigger/parser.js +126 -11
  120. package/dist/trigger/patch.js +12 -2
  121. package/dist/trigger/scheduler.js +444 -40
  122. package/dist/trigger/validation.js +13 -0
  123. package/dist/utils/aid-bind.js +43 -29
  124. package/dist/utils/error-dict.json +7 -0
  125. package/dist/utils/evolcore-version.js +21 -0
  126. package/dist/utils/instance-registry.js +14 -7
  127. package/dist/utils/log-writer.js +46 -0
  128. package/dist/utils/logger.js +2 -2
  129. package/dist/utils/media-cache.js +4 -1
  130. package/dist/utils/model-prices.jsonl +6 -3
  131. package/dist/utils/restart-safety.js +31 -0
  132. package/dist/utils/stable-semver.js +21 -0
  133. package/dist/utils/stats.js +33 -9
  134. package/dist/utils/system-memory.js +62 -0
  135. package/kits/docs/INDEX.md +2 -1
  136. package/kits/docs/channels/aun.md +4 -13
  137. package/kits/docs/evolcore/INDEX.md +5 -3
  138. package/kits/docs/evolcore/agent.md +9 -1
  139. package/kits/docs/evolcore/aid.md +5 -2
  140. package/kits/docs/evolcore/config.md +47 -2
  141. package/kits/docs/evolcore/contact.md +61 -0
  142. package/kits/docs/evolcore/fs.md +9 -0
  143. package/kits/docs/evolcore/group-rules.md +46 -4
  144. package/kits/docs/evolcore/group.md +15 -6
  145. package/kits/docs/evolcore/model.md +4 -1
  146. package/kits/docs/evolcore/msg.md +12 -6
  147. package/kits/docs/evolcore/response.md +16 -21
  148. package/kits/docs/evolcore/rpc.md +2 -0
  149. package/kits/docs/evolcore/stats.md +15 -2
  150. package/kits/docs/evolcore/storage.md +1 -0
  151. package/kits/docs/evolcore/trigger.md +42 -10
  152. package/kits/docs/path-registry.md +36 -17
  153. package/kits/eck_manifest.json +12 -0
  154. package/kits/eck_message_manifest.json +12 -1
  155. package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
  156. package/kits/rules/01-overview.md +17 -7
  157. package/kits/rules/02-navigation.md +38 -18
  158. package/kits/rules/03-identity.md +28 -24
  159. package/kits/rules/04-relation.md +44 -28
  160. package/kits/rules/05-venue.md +31 -15
  161. package/kits/rules/06-channel.md +11 -7
  162. package/kits/schemas/_meta.json +21 -7
  163. package/kits/schemas/agent-config.schema.6.json +322 -0
  164. package/kits/schemas/agent-config.schema.7.json +303 -0
  165. package/kits/schemas/agent-config.schema.8.json +304 -0
  166. package/kits/schemas/agent-config.schema.9.json +374 -0
  167. package/kits/schemas/contact-book.schema.2.json +43 -0
  168. package/kits/schemas/contact-book.schema.3.json +67 -0
  169. package/kits/schemas/daemon.schema.1.json +3 -2
  170. package/kits/schemas/daemon.schema.2.json +101 -0
  171. package/kits/schemas/daemon.schema.3.json +123 -0
  172. package/kits/schemas/defaults.schema.2.json +85 -0
  173. package/kits/schemas/defaults.schema.3.json +73 -0
  174. package/kits/schemas/relation-config.schema.5.json +47 -0
  175. package/kits/schemas/relation-config.schema.6.json +46 -0
  176. package/kits/schemas/relation-config.schema.7.json +59 -0
  177. package/kits/schemas/role-config.schema.1.json +1 -0
  178. package/kits/schemas/role-registry.schema.1.json +2 -2
  179. package/kits/schemas/single-session.schema.2.json +57 -0
  180. package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
  181. package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
  182. package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
  183. package/kits/templates/roles/admin.json +47 -0
  184. package/kits/templates/roles/member.json +5 -0
  185. package/kits/templates/roles/owner.json +1 -0
  186. package/kits/templates/roles/visitor.json +5 -0
  187. package/kits/templates/system-fragments/channel.md +12 -2
  188. package/kits/templates/system-fragments/commands.md +3 -1
  189. package/kits/templates/system-fragments/identity.md +3 -1
  190. package/kits/templates/system-fragments/relation.md +1 -1
  191. package/kits/templates/system-fragments/session.md +6 -2
  192. package/package.json +7 -5
  193. package/MIGRATION-0.5.0.md +0 -378
  194. package/ROLE_ACCESS_CONTROL.md +0 -174
  195. package/assets/brand/evolcore/README.md +0 -19
  196. package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
  197. package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
  198. package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
  199. package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
  200. package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
  201. package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
  202. package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
  203. package/assets/brand/evolcore/evolcore-logo.png +0 -0
  204. package/assets/brand/evolcore/evolcore-logo.svg +0 -14
  205. package/assets/brand/evolcore/evolcore-mark.png +0 -0
  206. package/assets/brand/evolcore/evolcore-mark.svg +0 -10
@@ -719,7 +719,7 @@ export class AgentRunner {
719
719
  if (!permCtx?.adapter || !permCtx?.channelId) {
720
720
  return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
721
721
  }
722
- const adapterHasInteractionPath = !!permCtx.adapter.send;
722
+ const adapterHasInteractionPath = permCtx.adapter.capabilities?.interaction === true;
723
723
  if (!adapterHasInteractionPath || !permCtx.interactionRouter) {
724
724
  return this.handleAskUserQuestionFallback(sessionId, input, questions, options);
725
725
  }
@@ -756,6 +756,7 @@ export class AgentRunner {
756
756
  },
757
757
  channelId: permCtx.channelId,
758
758
  sessionId,
759
+ initiatorId: permCtx.userId,
759
760
  };
760
761
  }
761
762
  else {
@@ -783,6 +784,7 @@ export class AgentRunner {
783
784
  },
784
785
  channelId: permCtx.channelId,
785
786
  sessionId,
787
+ initiatorId: permCtx.userId,
786
788
  };
787
789
  }
788
790
  let cardSent = false;
@@ -797,7 +799,10 @@ export class AgentRunner {
797
799
  replyContext: permCtx.replyContext,
798
800
  });
799
801
  const optionLines = q.options.map((o, idx) => ` ${idx + 1}. ${o.label}${o.description ? ` — ${o.description}` : ''}`).join('\n');
800
- const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}`;
802
+ const answerHint = q.multiSelect
803
+ ? '回复 /ask 1,2(多选用逗号分隔),或 /ask <自定义内容>'
804
+ : '回复 /ask 1,或 /ask <自定义内容>';
805
+ const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}\n\n${answerHint}`;
801
806
  const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
802
807
  cardSent = !!result;
803
808
  }
@@ -850,20 +855,6 @@ export class AgentRunner {
850
855
  const customText = values?.custom_text;
851
856
  finish({ kind: 'answered', value: typeof customText === 'string' && customText.trim() ? customText.trim() : null });
852
857
  }
853
- else if (action === '_show_input') {
854
- if (permCtx?.interceptNextMessage) {
855
- permCtx.interactionRouter?.markWaiting(sessionId);
856
- sendPrompt?.('✏️ 请直接发送你的自定义回复').catch(() => { });
857
- permCtx.interceptNextMessage(sessionId, (msg) => {
858
- permCtx.interactionRouter?.unmarkWaiting(sessionId);
859
- const text = (msg.content || '').trim();
860
- finish({ kind: 'answered', value: text || null });
861
- });
862
- }
863
- else {
864
- finish({ kind: 'cancelled', reason: 'explicit_cancel' });
865
- }
866
- }
867
858
  else if (action === 'submit' && q.multiSelect && values) {
868
859
  const selected = [];
869
860
  q.options.forEach((opt, idx) => {
@@ -895,7 +886,6 @@ export class AgentRunner {
895
886
  });
896
887
  if (settlement.kind === 'cancelled') {
897
888
  try {
898
- permCtx.cancelIntercept?.(sessionId);
899
889
  try {
900
890
  await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
901
891
  }
@@ -958,7 +948,9 @@ export class AgentRunner {
958
948
  command: 'ask',
959
949
  buttonArgMap: Object.fromEntries(q.options.map((_, i) => [`opt-${i}`, String(i + 1)])),
960
950
  acceptFreeText: true,
961
- freeTextHint: '或回复 /ask <自定义内容>',
951
+ freeTextHint: q.multiSelect
952
+ ? '多选请回复 /ask 1,2;也可回复 /ask <自定义内容>'
953
+ : '或回复 /ask <自定义内容>',
962
954
  },
963
955
  };
964
956
  await sendPrompt(renderActionAsText(interaction));
@@ -990,12 +982,25 @@ export class AgentRunner {
990
982
  finish({ kind: 'cancelled', reason: 'aborted' });
991
983
  };
992
984
  permCtx.interactionRouter.register(requestId, sessionId, (action) => {
993
- const num = parseInt(action.trim(), 10);
994
- if (num >= 1 && num <= q.options.length) {
995
- finish({ kind: 'answered', value: q.options[num - 1].label });
985
+ const answer = action.trim();
986
+ const selectionParts = answer.split(/[,,]/).map(part => part.trim());
987
+ const selectedIndexes = selectionParts.map(part => Number(part));
988
+ const hasValidNumericSelection = selectionParts.length > 0
989
+ && selectionParts.every((part, index) => (/^\d+$/.test(part)
990
+ && Number.isInteger(selectedIndexes[index])
991
+ && selectedIndexes[index] >= 1
992
+ && selectedIndexes[index] <= q.options.length));
993
+ if (q.multiSelect && hasValidNumericSelection) {
994
+ finish({
995
+ kind: 'answered',
996
+ value: [...new Set(selectedIndexes)].map(num => q.options[num - 1].label),
997
+ });
998
+ }
999
+ else if (!q.multiSelect && hasValidNumericSelection && selectedIndexes.length === 1) {
1000
+ finish({ kind: 'answered', value: q.options[selectedIndexes[0] - 1].label });
996
1001
  }
997
1002
  else {
998
- finish({ kind: 'answered', value: action.trim() });
1003
+ finish({ kind: 'answered', value: answer });
999
1004
  }
1000
1005
  }, {
1001
1006
  initiatorId: permCtx.userId,
@@ -1113,20 +1118,6 @@ export class AgentRunner {
1113
1118
  const feedback = typeof values?.custom_text === 'string' ? values.custom_text.trim() : '';
1114
1119
  deny(feedback || '用户提交了反馈');
1115
1120
  }
1116
- else if (trimmed === '_show_input') {
1117
- if (permCtx.interceptNextMessage) {
1118
- router.markWaiting(sessionId);
1119
- sendPrompt?.('✏️ 请直接发送你的反馈意见').catch(() => { });
1120
- permCtx.interceptNextMessage(sessionId, msg => {
1121
- router.unmarkWaiting(sessionId);
1122
- const feedback = (msg.content || '').trim();
1123
- deny(feedback || '用户提交了反馈');
1124
- });
1125
- }
1126
- else {
1127
- deny('用户提交了反馈');
1128
- }
1129
- }
1130
1121
  else if (trimmed === '2' || trimmed.toLowerCase() === 'reject' || trimmed === '拒绝') {
1131
1122
  deny('用户拒绝了计划');
1132
1123
  }
@@ -1153,7 +1144,6 @@ export class AgentRunner {
1153
1144
  });
1154
1145
  try {
1155
1146
  if (settlement.kind === 'cancelled') {
1156
- permCtx.cancelIntercept?.(sessionId);
1157
1147
  try {
1158
1148
  await permCtx.turn?.onInteractionSettled?.(requestId, settlement.reason === 'timeout' ? 'timed_out' : 'cancelled');
1159
1149
  }
@@ -1181,7 +1171,7 @@ export class AgentRunner {
1181
1171
  };
1182
1172
  // 尝试发送交互卡片
1183
1173
  let cardSent = false;
1184
- if (permCtx.adapter?.send) {
1174
+ if (permCtx.adapter?.capabilities?.interaction === true) {
1185
1175
  // 发送计划内容:找 plans 目录中最新修改的 .md 文件
1186
1176
  if (sendPrompt) {
1187
1177
  try {
@@ -1283,10 +1273,20 @@ export class AgentRunner {
1283
1273
  * SDK 原始事件 → 标准 AgentEvent 转换
1284
1274
  * 所有 SDK 特有的事件类型引用封装在此方法内
1285
1275
  */
1286
- async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel) {
1276
+ async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false) {
1287
1277
  let lastSessionId;
1278
+ let hasTurnActivity = false;
1279
+ let ignoredPreTurnResult = false;
1280
+ let waitingForBackgroundCompletion = false;
1281
+ let liveBackgroundTasks = new Set();
1282
+ const unsettledTasks = new Set();
1283
+ const terminalTasks = new Set();
1284
+ const ambientTasks = new Set();
1285
+ const awaitedTaskNotifications = new Set();
1286
+ const taskMetadata = new Map();
1288
1287
  // tool_use_id → tool_name 映射,用于从 SDKUserMessage 的 tool_result 块中还原工具名
1289
1288
  const toolUseNames = new Map();
1289
+ const emittedToolResultIds = new Set();
1290
1290
  let turnCount = 0;
1291
1291
  const seenMessageIds = new Set();
1292
1292
  let lastModelCall;
@@ -1302,11 +1302,99 @@ export class AgentRunner {
1302
1302
  yield { type: 'session_id', sessionId: event.session_id };
1303
1303
  }
1304
1304
  if (event.type === 'user' && event.uuid === inputId) {
1305
+ hasTurnActivity = true;
1305
1306
  yield { type: 'input_accepted', inputId };
1306
1307
  }
1308
+ if (event.type === 'system' && event.subtype === 'task_started' && typeof event.task_id === 'string') {
1309
+ const taskKind = event.subagent_type || event.task_type === 'remote_agent'
1310
+ ? 'agent'
1311
+ : event.workflow_name || event.task_type === 'local_workflow'
1312
+ ? 'workflow'
1313
+ : 'background';
1314
+ taskMetadata.set(event.task_id, {
1315
+ taskKind,
1316
+ description: event.description,
1317
+ subagentType: event.subagent_type,
1318
+ taskType: event.task_type,
1319
+ });
1320
+ if (event.skip_transcript === true) {
1321
+ ambientTasks.add(event.task_id);
1322
+ unsettledTasks.delete(event.task_id);
1323
+ liveBackgroundTasks.delete(event.task_id);
1324
+ }
1325
+ else if (!terminalTasks.has(event.task_id)) {
1326
+ unsettledTasks.add(event.task_id);
1327
+ hasTurnActivity = true;
1328
+ }
1329
+ yield {
1330
+ type: 'task_started',
1331
+ taskId: event.task_id,
1332
+ taskKind,
1333
+ toolUseId: event.tool_use_id,
1334
+ description: event.description || event.task_id,
1335
+ subagentType: event.subagent_type,
1336
+ taskType: event.task_type,
1337
+ workflowName: event.workflow_name,
1338
+ prompt: event.prompt,
1339
+ skipTranscript: event.skip_transcript,
1340
+ };
1341
+ continue;
1342
+ }
1343
+ if (event.type === 'system' && event.subtype === 'background_tasks_changed') {
1344
+ const tasks = Array.isArray(event.tasks) ? event.tasks : [];
1345
+ liveBackgroundTasks = new Set(tasks
1346
+ .map((task) => task?.task_id)
1347
+ .filter((taskId) => (typeof taskId === 'string'
1348
+ && !terminalTasks.has(taskId)
1349
+ && !ambientTasks.has(taskId))));
1350
+ yield {
1351
+ type: 'background_tasks_changed',
1352
+ tasks: tasks.map((task) => ({
1353
+ taskId: task.task_id,
1354
+ taskKind: task.task_type === 'remote_agent'
1355
+ ? 'agent'
1356
+ : task.task_type === 'local_workflow'
1357
+ ? 'workflow'
1358
+ : taskMetadata.get(task.task_id)?.taskKind || 'background',
1359
+ taskType: task.task_type,
1360
+ description: task.description,
1361
+ })),
1362
+ };
1363
+ continue;
1364
+ }
1365
+ if (event.type === 'system' && event.subtype === 'task_notification' && typeof event.task_id === 'string') {
1366
+ const metadata = taskMetadata.get(event.task_id);
1367
+ const taskKind = metadata?.taskKind
1368
+ || (event.tool_use_id && toolUseNames.get(event.tool_use_id) === 'Agent' ? 'agent' : undefined)
1369
+ || (typeof event.summary === 'string' && /^Agent\b/.test(event.summary) ? 'agent' : 'background');
1370
+ terminalTasks.add(event.task_id);
1371
+ ambientTasks.delete(event.task_id);
1372
+ unsettledTasks.delete(event.task_id);
1373
+ liveBackgroundTasks.delete(event.task_id);
1374
+ awaitedTaskNotifications.delete(event.task_id);
1375
+ yield {
1376
+ type: 'task_notification',
1377
+ taskId: event.task_id,
1378
+ taskKind,
1379
+ toolUseId: event.tool_use_id,
1380
+ description: metadata?.description,
1381
+ subagentType: metadata?.subagentType,
1382
+ status: event.status || 'completed',
1383
+ outputFile: event.output_file,
1384
+ summary: event.summary,
1385
+ usage: event.usage ? {
1386
+ totalTokens: event.usage.total_tokens,
1387
+ toolUses: event.usage.tool_uses,
1388
+ durationMs: event.usage.duration_ms,
1389
+ } : undefined,
1390
+ skipTranscript: event.skip_transcript,
1391
+ };
1392
+ continue;
1393
+ }
1307
1394
  if (event.type === 'stream_event') {
1308
1395
  const streamEvent = event.event;
1309
1396
  if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
1397
+ hasTurnActivity = true;
1310
1398
  lastModelCall = {
1311
1399
  uuid: event.uuid,
1312
1400
  model: streamEvent.message.model,
@@ -1347,11 +1435,21 @@ export class AgentRunner {
1347
1435
  }
1348
1436
  // system: task_progress → task_progress
1349
1437
  if (event.type === 'system' && event.subtype === 'task_progress') {
1438
+ const metadata = typeof event.task_id === 'string' ? taskMetadata.get(event.task_id) : undefined;
1439
+ if (!event.task_id || !ambientTasks.has(event.task_id))
1440
+ hasTurnActivity = true;
1350
1441
  yield {
1351
1442
  type: 'task_progress',
1443
+ taskId: event.task_id,
1444
+ taskKind: metadata?.taskKind || (event.subagent_type ? 'agent' : undefined),
1445
+ toolUseId: event.tool_use_id,
1446
+ description: event.description,
1447
+ subagentType: event.subagent_type || metadata?.subagentType,
1352
1448
  summary: event.summary,
1353
- toolUses: event.tool_uses,
1354
- durationMs: event.duration_ms,
1449
+ lastToolName: event.last_tool_name,
1450
+ totalTokens: event.usage?.total_tokens,
1451
+ toolUses: event.usage?.tool_uses ?? event.tool_uses,
1452
+ durationMs: event.usage?.duration_ms ?? event.duration_ms,
1355
1453
  };
1356
1454
  }
1357
1455
  // system: session_state_changed → state_changed
@@ -1360,6 +1458,7 @@ export class AgentRunner {
1360
1458
  }
1361
1459
  // assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
1362
1460
  if (event.type === 'assistant' && event.message?.content) {
1461
+ hasTurnActivity = true;
1363
1462
  lastAssistantUuid = event.uuid ?? lastAssistantUuid;
1364
1463
  const msgId = event.message.id;
1365
1464
  if (!msgId || !seenMessageIds.has(msgId)) {
@@ -1410,6 +1509,8 @@ export class AgentRunner {
1410
1509
  const resultContent = typeof block.content === 'string'
1411
1510
  ? block.content
1412
1511
  : block.content != null ? JSON.stringify(block.content) : '';
1512
+ if (block.tool_use_id)
1513
+ emittedToolResultIds.add(block.tool_use_id);
1413
1514
  yield {
1414
1515
  type: 'tool_result',
1415
1516
  name: toolName,
@@ -1423,15 +1524,22 @@ export class AgentRunner {
1423
1524
  }
1424
1525
  // result → complete(含 permission_denials 提取)
1425
1526
  if (event.type === 'result') {
1426
- // 先发出被拒绝的权限事件
1527
+ // Most SDK streams surface denials as user-message tool_result blocks.
1528
+ // The terminal summary is only a fallback for incomplete streams.
1427
1529
  if (Array.isArray(event.permission_denials)) {
1428
1530
  for (const denial of event.permission_denials) {
1531
+ const callId = typeof denial.tool_use_id === 'string' ? denial.tool_use_id : undefined;
1532
+ if (callId && emittedToolResultIds.has(callId))
1533
+ continue;
1534
+ if (callId)
1535
+ emittedToolResultIds.add(callId);
1429
1536
  yield {
1430
1537
  type: 'tool_result',
1431
1538
  name: denial.tool_name || '',
1432
1539
  result: '',
1433
1540
  isError: true,
1434
- error: `权限被拒绝: ${denial.tool_name}`,
1541
+ error: `权限被拒绝: ${denial.tool_name || 'unknown tool'}`,
1542
+ callId,
1435
1543
  };
1436
1544
  }
1437
1545
  }
@@ -1502,7 +1610,7 @@ export class AgentRunner {
1502
1610
  // 降级:无逐次数据,写一条累计行
1503
1611
  modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
1504
1612
  }
1505
- yield {
1613
+ const completeEvent = {
1506
1614
  type: 'complete',
1507
1615
  result: cleanResult,
1508
1616
  subtype: event.subtype,
@@ -1520,10 +1628,53 @@ export class AgentRunner {
1520
1628
  modelCalls,
1521
1629
  assistantUuid: lastAssistantUuid,
1522
1630
  };
1523
- // result SDK 流的终结事件,不再等待后续(防止 interrupt 后流不关闭导致挂起)
1631
+ // Fatal results belong to the query transport itself and must terminate
1632
+ // even when the current input was not echoed back by the CLI.
1633
+ if (event.is_error) {
1634
+ yield { ...completeEvent, queryFinal: true };
1635
+ return;
1636
+ }
1637
+ if (isResuming && !hasTurnActivity && !ignoredPreTurnResult) {
1638
+ ignoredPreTurnResult = true;
1639
+ logger.info(`[AgentRunner] Ignoring pre-turn result while resuming session=${sessionId} input=${inputId}`);
1640
+ continue;
1641
+ }
1642
+ const hasLiveTasks = liveBackgroundTasks.size > 0 || unsettledTasks.size > 0;
1643
+ if (hasLiveTasks) {
1644
+ waitingForBackgroundCompletion = true;
1645
+ for (const taskId of liveBackgroundTasks)
1646
+ awaitedTaskNotifications.add(taskId);
1647
+ for (const taskId of unsettledTasks)
1648
+ awaitedTaskNotifications.add(taskId);
1649
+ yield { ...completeEvent, queryFinal: false };
1650
+ hasTurnActivity = false;
1651
+ ignoredPreTurnResult = false;
1652
+ continue;
1653
+ }
1654
+ // background_tasks_changed may report an empty set before the matching
1655
+ // task_notification. Do not close in that gap: the notification wakes the
1656
+ // parent agent for the final synthesis turn.
1657
+ if (waitingForBackgroundCompletion && awaitedTaskNotifications.size > 0) {
1658
+ yield { ...completeEvent, queryFinal: false };
1659
+ hasTurnActivity = false;
1660
+ ignoredPreTurnResult = false;
1661
+ continue;
1662
+ }
1663
+ yield { ...completeEvent, queryFinal: true };
1524
1664
  return;
1525
1665
  }
1526
1666
  }
1667
+ if (waitingForBackgroundCompletion) {
1668
+ yield {
1669
+ type: 'complete',
1670
+ isError: true,
1671
+ subtype: 'protocol_incomplete',
1672
+ terminalReason: 'background_tasks_incomplete',
1673
+ protocolIncompleteReason: 'background_tasks_incomplete',
1674
+ errors: ['Claude SDK stream ended before background tasks produced a final parent result'],
1675
+ queryFinal: true,
1676
+ };
1677
+ }
1527
1678
  }
1528
1679
  catch (err) {
1529
1680
  // 子进程崩溃(如 exited with code 1)时,把缓冲的 stderr 打出来还原真实原因。
@@ -1705,7 +1856,7 @@ export class AgentRunner {
1705
1856
  }
1706
1857
  // These tools need the dedicated canUseTool interaction handlers below.
1707
1858
  if (toolName === 'AskUserQuestion' || toolName === 'ExitPlanMode') {
1708
- return hookDecision('defer', undefined, updatedInput);
1859
+ return hookDecision('ask', undefined, updatedInput);
1709
1860
  }
1710
1861
  if (callPermissionMode === 'readonly') {
1711
1862
  const permCtx = this.permissionContexts.get(sessionId);
@@ -2107,7 +2258,6 @@ export class AgentRunner {
2107
2258
  logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
2108
2259
  logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
2109
2260
  msgStream.push(prompt, images, inputId);
2110
- msgStream.end();
2111
2261
  sdkStream = createQuery(msgStream);
2112
2262
  }
2113
2263
  else {
@@ -2124,7 +2274,7 @@ export class AgentRunner {
2124
2274
  this.interruptFns.set(sessionId, () => sdkStream.interrupt());
2125
2275
  }
2126
2276
  // 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
2127
- const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel);
2277
+ const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId);
2128
2278
  const self = this;
2129
2279
  return (async function* () {
2130
2280
  try {
@@ -167,6 +167,74 @@ export class CodexAppServerClient {
167
167
  await this.request('thread/metadata/update', params);
168
168
  return true;
169
169
  }
170
+ async threadShellCommand(threadId, command, timeoutMs = 10_000) {
171
+ let commandItemId;
172
+ let output = '';
173
+ let settle;
174
+ let fail;
175
+ const completion = new Promise((resolve, reject) => {
176
+ settle = resolve;
177
+ fail = reject;
178
+ });
179
+ const unsubscribe = this.onNotification(notification => {
180
+ const params = (notification.params || {});
181
+ const notificationThreadId = params.threadId ?? params.thread_id;
182
+ if (notificationThreadId !== threadId)
183
+ return;
184
+ const item = params.item && typeof params.item === 'object'
185
+ ? params.item
186
+ : undefined;
187
+ const itemId = typeof item?.id === 'string'
188
+ ? item.id
189
+ : typeof params.itemId === 'string'
190
+ ? params.itemId
191
+ : undefined;
192
+ const source = item?.source;
193
+ const isUserShell = source === 'userShell' || source === 'user_shell';
194
+ if (notification.method === 'item/started'
195
+ && item?.type === 'commandExecution'
196
+ && isUserShell
197
+ && (item.command === command || commandItemId === undefined)) {
198
+ commandItemId = itemId;
199
+ return;
200
+ }
201
+ if (notification.method === 'item/commandExecution/outputDelta'
202
+ && commandItemId
203
+ && itemId === commandItemId
204
+ && typeof params.delta === 'string') {
205
+ output += params.delta;
206
+ return;
207
+ }
208
+ if (notification.method === 'item/completed'
209
+ && item?.type === 'commandExecution'
210
+ && isUserShell
211
+ && (!commandItemId || itemId === commandItemId)) {
212
+ const aggregated = typeof item.aggregatedOutput === 'string'
213
+ ? item.aggregatedOutput
214
+ : typeof item.aggregated_output === 'string'
215
+ ? item.aggregated_output
216
+ : output;
217
+ const exitCode = typeof item.exitCode === 'number'
218
+ ? item.exitCode
219
+ : typeof item.exit_code === 'number'
220
+ ? item.exit_code
221
+ : undefined;
222
+ settle({ output: aggregated, ...(exitCode !== undefined ? { exitCode } : {}) });
223
+ }
224
+ });
225
+ const timer = setTimeout(() => {
226
+ fail(new Error(`thread/shellCommand timed out for thread ${threadId}`));
227
+ }, timeoutMs);
228
+ timer.unref?.();
229
+ try {
230
+ await this.request('thread/shellCommand', { threadId, command });
231
+ return await completion;
232
+ }
233
+ finally {
234
+ clearTimeout(timer);
235
+ unsubscribe();
236
+ }
237
+ }
170
238
  async turnInterrupt(threadId, turnId) {
171
239
  await this.request('turn/interrupt', { threadId, turnId });
172
240
  return true;