evolcore 0.0.24 → 0.0.26

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 (112) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/bin/ec.js +12 -1
  3. package/bin/install-codex-managed-hooks.mjs +28 -1
  4. package/dist/agents/baseagent.js +96 -46
  5. package/dist/agents/claude-runner.js +54 -13
  6. package/dist/agents/codex/app-server-client.js +13 -5
  7. package/dist/agents/codex/catalog.js +1 -1
  8. package/dist/agents/codex/diagnostics.js +34 -1
  9. package/dist/agents/codex-runner.js +128 -42
  10. package/dist/agents/ecagent-runner.js +14 -18
  11. package/dist/agents/gemini-runner.js +24 -9
  12. package/dist/agents/runner-types.js +25 -17
  13. package/dist/aun/aid/agentmd.js +135 -10
  14. package/dist/aun/aid/client.js +1 -1
  15. package/dist/aun/aid/identity.js +1 -1
  16. package/dist/aun/aid/index.js +2 -1
  17. package/dist/aun/aid/managed-operation.js +53 -1
  18. package/dist/aun/aid/store.js +17 -13
  19. package/dist/aun/menu-client.js +74 -0
  20. package/dist/aun/msg/group.js +1 -1
  21. package/dist/aun/msg/managed-operation.js +1 -1
  22. package/dist/aun/msg/p2p.js +6 -1
  23. package/dist/aun/msg/upload.js +30 -22
  24. package/dist/aun/storage/client.js +4 -1
  25. package/dist/aun/storage/index.js +1 -1
  26. package/dist/channels/aun.js +329 -94
  27. package/dist/cli/agent-command.js +125 -5
  28. package/dist/cli/agent-create.js +151 -11
  29. package/dist/cli/agent.js +119 -6
  30. package/dist/cli/aun-commands.js +74 -7
  31. package/dist/cli/config.js +27 -8
  32. package/dist/cli/contact.js +63 -6
  33. package/dist/cli/daemon-commands.js +90 -25
  34. package/dist/cli/diag.js +31 -0
  35. package/dist/cli/handoff-command.js +35 -3
  36. package/dist/cli/index.js +7 -1
  37. package/dist/cli/managed-command-guard.js +3 -0
  38. package/dist/cli/menu-client.js +38 -23
  39. package/dist/cli/restart-monitor.js +57 -10
  40. package/dist/config/builtin-roles.js +2 -0
  41. package/dist/config/config-manager.js +15 -0
  42. package/dist/config/config-operation-service.js +165 -3
  43. package/dist/config/contact-operation-service.js +96 -7
  44. package/dist/config/resolved-config-op.js +66 -8
  45. package/dist/core/agent-application-service.js +2 -1
  46. package/dist/core/auth/authorization-audit.js +20 -12
  47. package/dist/core/auth/operation-authorizer.js +87 -8
  48. package/dist/core/auth/operation-catalog.js +51 -4
  49. package/dist/core/auth/remote-access-store.js +40 -1
  50. package/dist/core/bootstrap-service.js +1 -1
  51. package/dist/core/command/agent-control.js +54 -25
  52. package/dist/core/command/cli-intent-parser.js +18 -0
  53. package/dist/core/command/command-handler.js +152 -20
  54. package/dist/core/command/diag-service.js +118 -0
  55. package/dist/core/command/menu-catalog.js +13 -3
  56. package/dist/core/command/menu-handler.js +88 -31
  57. package/dist/core/command/menu-protocol.js +6 -0
  58. package/dist/core/command/menu-token-store.js +41 -8
  59. package/dist/core/command/slash-gate.js +34 -4
  60. package/dist/core/command/slash-handler.js +77 -12
  61. package/dist/core/event-catalog.js +5 -0
  62. package/dist/core/evolagent.js +3 -3
  63. package/dist/core/handoff/runtime.js +205 -27
  64. package/dist/core/handoff/store.js +193 -4
  65. package/dist/core/message/im-renderer.js +72 -21
  66. package/dist/core/message/message-bridge.js +69 -9
  67. package/dist/core/message/message-queue.js +26 -0
  68. package/dist/core/message/message-utils.js +1 -0
  69. package/dist/core/message/response-engine.js +48 -23
  70. package/dist/core/model/catalog-service.js +79 -49
  71. package/dist/core/model/context-window-suffix.js +33 -0
  72. package/dist/core/model/model-catalog.js +1 -1
  73. package/dist/core/model/model-permission.js +12 -3
  74. package/dist/core/permission/ec-command-parser.js +51 -12
  75. package/dist/core/permission/sandbox-runtime.js +36 -14
  76. package/dist/core/permission/tool-policy.js +47 -7
  77. package/dist/index.js +195 -31
  78. package/dist/ipc.js +54 -2
  79. package/dist/response-system/engines/v1/proactive-flow.js +59 -2
  80. package/dist/stats/billing.js +41 -15
  81. package/dist/stats/price-resolver.js +28 -10
  82. package/dist/utils/avatar-upload.js +282 -23
  83. package/dist/utils/cross-platform.js +30 -1
  84. package/dist/utils/error-utils.js +21 -0
  85. package/dist/utils/evolcore-version.js +129 -0
  86. package/dist/utils/logger.js +3 -1
  87. package/dist/utils/model-prices.jsonl +3 -3
  88. package/dist/utils/npm-ops.js +176 -35
  89. package/kits/docs/context-assembly.md +1 -0
  90. package/kits/docs/evolcore/INDEX.md +3 -1
  91. package/kits/docs/evolcore/agent-create.md +30 -11
  92. package/kits/docs/evolcore/agent.md +18 -8
  93. package/kits/docs/evolcore/aid.md +6 -4
  94. package/kits/docs/evolcore/codex-windows-sandbox.md +146 -0
  95. package/kits/docs/evolcore/config.md +7 -0
  96. package/kits/docs/evolcore/contact.md +12 -5
  97. package/kits/docs/evolcore/menu-client.md +11 -3
  98. package/kits/docs/evolcore/msg.md +1 -0
  99. package/kits/docs/prompt-loading-architecture.md +1 -0
  100. package/kits/docs/venues/group.md +12 -0
  101. package/kits/eck_manifest.json +12 -0
  102. package/kits/rules/03-identity.md +3 -1
  103. package/kits/templates/message-fragments/handoff-request-to-target.md +3 -3
  104. package/kits/templates/roles/admin.json +14 -2
  105. package/kits/templates/roles/member.json +3 -2
  106. package/kits/templates/roles/visitor.json +4 -3
  107. package/kits/templates/system-fragments/bootstrap.md +16 -5
  108. package/kits/templates/system-fragments/channel.md +1 -1
  109. package/kits/templates/system-fragments/session.md +1 -1
  110. package/package.json +6 -4
  111. package/skills/eclink/SKILL.md +0 -84
  112. package/skills/eclink/agents/openai.yaml +0 -4
@@ -1,4 +1,6 @@
1
1
  import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
+ // Codex/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
+ import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
2
4
  import { hasTrustedFullAccessContext } from './runner-types.js';
3
5
  import { requestDangerousCommandPermission } from '../core/permission/approval-gateway.js';
4
6
  import { checkReadonly, checkDangerousCommand, evaluateToolPreflight } from '../core/permission/tool-policy.js';
@@ -9,7 +11,7 @@ import { resolveOpenaiConfig, resolveCodexCliPath } from './baseagent.js';
9
11
  import { logger } from '../utils/logger.js';
10
12
  import { summarizeToolInputForAudit } from '../utils/tool-summary.js';
11
13
  import { auditCodexApprovalDecision, auditPermissionLifecycle, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
12
- import { isRetryableError } from '../utils/error-utils.js';
14
+ import { classifyError, ErrorType, isOutputTokenLimitError, isRetryableError } from '../utils/error-utils.js';
13
15
  import { renderActionAsText } from '../core/interaction-router.js';
14
16
  import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
15
17
  import { resolveCodexCapabilityThreadConfigForProject } from '../core/capability/capability-manager.js';
@@ -36,19 +38,22 @@ const MIME_EXT = {
36
38
  'image/webp': '.webp',
37
39
  };
38
40
  const APPROVAL_LOG_SUMMARY_MAX_LENGTH = 256;
41
+ function toAgentErrorType(message) {
42
+ switch (classifyError(new Error(message))) {
43
+ case ErrorType.CONTEXT_TOO_LONG: return 'context_too_long';
44
+ case ErrorType.AUTH_ERROR: return 'auth';
45
+ case ErrorType.API_ERROR:
46
+ case ErrorType.STREAM_ERROR:
47
+ case ErrorType.SDK_TIMEOUT: return 'network';
48
+ default: return 'unknown';
49
+ }
50
+ }
39
51
  export function formatApprovalLogSummary(summary) {
40
52
  const singleLine = summary.replace(/\s+/gu, ' ').trim();
41
53
  if (singleLine.length <= APPROVAL_LOG_SUMMARY_MAX_LENGTH)
42
54
  return singleLine;
43
55
  return singleLine.slice(0, APPROVAL_LOG_SUMMARY_MAX_LENGTH - 3).trimEnd() + '...';
44
56
  }
45
- /**
46
- * 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
47
- * Codex/OpenAI API 不认识这些后缀,必须传入标准模型 ID
48
- */
49
- function stripContextWindowSuffix(modelId) {
50
- return modelId.replace(/\[(?:1m|200k)\]$/i, '');
51
- }
52
57
  // ── Codex 模型目录(动态获取,含 effort) ──
53
58
  class AsyncEventQueue {
54
59
  queue = [];
@@ -198,11 +203,13 @@ function parseManagedPreToolUseDenial(value) {
198
203
  .trim();
199
204
  if (!reason)
200
205
  reason = 'EvolCore PreToolUse policy denied this tool call';
201
- const policyCode = /工具调用已达到\s*\d+\s*次/.test(reason)
202
- ? 'proactive_tool_report_required'
203
- : /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
204
- ? 'proactive_first_send_required'
205
- : 'managed_pre_tool_use_denied';
206
+ const policyCode = /当前汇报命令(?:未通过规范解析|不符合当前会话)/.test(reason)
207
+ ? 'proactive_send_invalid'
208
+ : /工具调用已达到\s*\d+\s*次/.test(reason)
209
+ ? 'proactive_tool_report_required'
210
+ : /请先用\s+ec\s+(?:group\s+send|msg\s+send)/.test(reason)
211
+ ? 'proactive_first_send_required'
212
+ : 'managed_pre_tool_use_denied';
206
213
  return { policyCode, reason };
207
214
  }
208
215
  function outputTextForManagedHook(value) {
@@ -442,6 +449,7 @@ export class CodexRunner {
442
449
  return existing;
443
450
  const client = new CodexAppServerClient({
444
451
  apiKey: this.resolvedConfig.apiKey,
452
+ authType: this.resolvedConfig.authType,
445
453
  baseUrl: this.resolvedConfig.baseUrl,
446
454
  provider: this.resolvedConfig.provider,
447
455
  headers: this.resolvedConfig.headers,
@@ -1298,7 +1306,7 @@ export class CodexRunner {
1298
1306
  }
1299
1307
  return Object.fromEntries([...paths].map(candidate => [candidate, 'write']));
1300
1308
  }
1301
- buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath) {
1309
+ buildPermissionProfileConfig(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
1302
1310
  const profileName = this.permissionProfileName(sessionId);
1303
1311
  const managedTempDir = this.ensureSessionTempDir(sessionId);
1304
1312
  const network = {
@@ -1324,6 +1332,9 @@ export class CodexRunner {
1324
1332
  ...(bootstrapAgentMdPath ? {
1325
1333
  [path.resolve(bootstrapAgentMdPath)]: mode === 'readonly' ? 'read' : 'write',
1326
1334
  } : {}),
1335
+ ...(personalMdPath ? {
1336
+ [path.resolve(personalMdPath)]: mode === 'readonly' ? 'read' : 'write',
1337
+ } : {}),
1327
1338
  ...(mode !== 'readonly' && managedTempDir ? {
1328
1339
  [managedTempDir]: 'write',
1329
1340
  [managedTempDir + '/**']: 'write',
@@ -1336,12 +1347,12 @@ export class CodexRunner {
1336
1347
  },
1337
1348
  };
1338
1349
  }
1339
- buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath) {
1350
+ buildExecutionSandboxOptions(sessionId, mode, projectPath, bootstrapAgentMdPath, personalMdPath) {
1340
1351
  const decision = resolvePhaseOneExecutionSandbox(mode, this.permissionContexts.get(sessionId)?.role);
1341
1352
  return {
1342
1353
  decision,
1343
1354
  permissionProfileConfig: decision.state === 'active'
1344
- ? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath)
1355
+ ? this.buildPermissionProfileConfig(sessionId, decision.permissionMode, projectPath, bootstrapAgentMdPath, personalMdPath)
1345
1356
  : undefined,
1346
1357
  sandbox: decision.state === 'off' ? 'danger-full-access' : undefined,
1347
1358
  };
@@ -1758,7 +1769,7 @@ export class CodexRunner {
1758
1769
  }
1759
1770
  this.chatModes.set(sessionId, callMode);
1760
1771
  const callApprovalPolicy = this.toApprovalPolicy(callMode);
1761
- const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath);
1772
+ const executionSandbox = this.buildExecutionSandboxOptions(sessionId, callMode, projectPath, modelOverride?.bootstrapAgentMdPath, modelOverride?.personalMdPath);
1762
1773
  const appServer = this.getAppServerClient(callMode === 'fullaccess');
1763
1774
  this.dropExpiredPendingInterrupt(sessionId);
1764
1775
  const effectiveApprovalPolicy = callApprovalPolicy;
@@ -2604,7 +2615,7 @@ export class CodexRunner {
2604
2615
  release();
2605
2616
  }
2606
2617
  }
2607
- async forkSessionLocked(agentSessionId, projectPath, title) {
2618
+ async forkSessionLocked(agentSessionId, projectPath, title, lastTurnId) {
2608
2619
  const sessionKey = this.findSessionKeyByThread(agentSessionId);
2609
2620
  const mode = this.chatModes.get(sessionKey) ?? this.currentMode;
2610
2621
  const executionSandbox = this.buildExecutionSandboxOptions(sessionKey, mode, projectPath);
@@ -2622,6 +2633,7 @@ export class CodexRunner {
2622
2633
  approvalsReviewer: 'user',
2623
2634
  ...(executionSandbox.sandbox ? { sandbox: executionSandbox.sandbox } : {}),
2624
2635
  config: this.sanitizeThreadEnvironmentConfig(this.mergeThreadConfig(executionSandbox.permissionProfileConfig, capabilityConfig, externalToolConfig, this.buildEvolcoreShellEnvironmentConfig(sessionKey, delegationCarrier), this.buildLifecycleLockdownConfig(mode)), mode),
2636
+ ...(lastTurnId ? { lastTurnId } : {}),
2625
2637
  });
2626
2638
  const forkedThreadId = response.thread?.id;
2627
2639
  if (!forkedThreadId)
@@ -2636,29 +2648,80 @@ export class CodexRunner {
2636
2648
  async forkSessionAt(agentSessionId, projectPath, assistantMessageId, title) {
2637
2649
  const release = await this.acquireThreadOperation(this.threadOperationKey(agentSessionId), 'forkAt');
2638
2650
  try {
2639
- const turns = buildSessionTurnList(await this.getSessionMessages(agentSessionId, projectPath));
2651
+ const appServer = this.getAppServerClientForThread(agentSessionId);
2652
+ const history = await this.readForkHistory(appServer, agentSessionId);
2653
+ const messages = this.mapThreadToSessionMessages(history, agentSessionId);
2654
+ const turns = buildSessionTurnList(messages);
2640
2655
  const targetIndex = turns.findIndex(turn => turn.assistantUuid === assistantMessageId);
2641
2656
  if (targetIndex < 0)
2642
2657
  throw new Error('fork point not found');
2643
- const forkedThreadId = await this.forkSessionLocked(agentSessionId, projectPath, title);
2644
- const turnsToDiscard = turns.length - targetIndex - 1;
2645
- if (turnsToDiscard > 0) {
2646
- const rollbackRelease = await this.acquireThreadOperation(this.threadOperationKey(forkedThreadId), 'rollback');
2647
- try {
2648
- const rolledBack = await this.rollbackSessionTurnsLocked(forkedThreadId, turnsToDiscard);
2649
- if (!rolledBack)
2650
- throw new Error('forked thread rollback failed');
2651
- }
2652
- finally {
2653
- rollbackRelease();
2654
- }
2655
- }
2656
- return forkedThreadId;
2658
+ const targetTurnId = this.findTurnIdForAssistantMessage(history, assistantMessageId);
2659
+ if (!targetTurnId)
2660
+ throw new Error('fork point turn not found');
2661
+ return await this.forkSessionLocked(agentSessionId, projectPath, title, targetTurnId);
2657
2662
  }
2658
2663
  finally {
2659
2664
  release();
2660
2665
  }
2661
2666
  }
2667
+ findTurnIdForAssistantMessage(response, assistantMessageId) {
2668
+ for (const turn of response.thread?.turns ?? []) {
2669
+ if (typeof turn.id !== 'string' || !turn.id)
2670
+ continue;
2671
+ for (const item of this.getTurnItems(turn)) {
2672
+ if (item.type === 'agentMessage' && item.id === assistantMessageId)
2673
+ return turn.id;
2674
+ }
2675
+ // Older app-server histories may omit item ids; the mapper then uses the
2676
+ // turn id as the assistant message id fallback.
2677
+ if (turn.id === assistantMessageId)
2678
+ return turn.id;
2679
+ }
2680
+ return undefined;
2681
+ }
2682
+ async readForkHistory(appServer, threadId) {
2683
+ let directRead;
2684
+ let directReadError;
2685
+ try {
2686
+ directRead = await appServer.threadRead(threadId, true);
2687
+ if ((directRead.thread?.turns?.length ?? 0) > 0)
2688
+ return directRead;
2689
+ }
2690
+ catch (error) {
2691
+ directReadError = error;
2692
+ }
2693
+ // Paginated threads reject full-history thread/read. Hydrate turns through
2694
+ // the experimental paging RPC, retaining full items so assistant message
2695
+ // IDs can be mapped to their containing turn.
2696
+ const turns = [];
2697
+ let cursor;
2698
+ const seenCursors = new Set();
2699
+ try {
2700
+ do {
2701
+ const page = await appServer.threadTurnsList(threadId, 100, cursor);
2702
+ if (Array.isArray(page.data)) {
2703
+ turns.push(...page.data.filter((turn) => !!turn && typeof turn === 'object'));
2704
+ }
2705
+ const nextCursor = typeof page.nextCursor === 'string' && page.nextCursor ? page.nextCursor : undefined;
2706
+ if (!nextCursor || seenCursors.has(nextCursor))
2707
+ break;
2708
+ seenCursors.add(nextCursor);
2709
+ cursor = nextCursor;
2710
+ } while (true);
2711
+ }
2712
+ catch (error) {
2713
+ if (directReadError)
2714
+ throw directReadError;
2715
+ throw error;
2716
+ }
2717
+ // Pages are returned newest-first when following `nextCursor`; restore
2718
+ // chronological order before mapping turns to session messages.
2719
+ if (turns.length > 0)
2720
+ return { thread: { ...(directRead?.thread ?? {}), id: threadId, turns: turns.reverse() } };
2721
+ if (directRead)
2722
+ return directRead;
2723
+ throw directReadError instanceof Error ? directReadError : new Error('Codex thread history unavailable');
2724
+ }
2662
2725
  async setSessionName(agentSessionId, name) {
2663
2726
  return this.getAppServerClientForThread(agentSessionId).threadSetName(agentSessionId, name);
2664
2727
  }
@@ -4412,24 +4475,46 @@ export class CodexRunner {
4412
4475
  this.clearPendingToolDenialsForTurn(state.threadId, turnId);
4413
4476
  yield* this.reconcileOpenAppServerToolCalls(state, turnId);
4414
4477
  if (turn.status === 'failed' && turn.error?.message) {
4478
+ logger.warn(`[CodexRunner] turn failed: ${JSON.stringify({
4479
+ sessionId, threadId: state.threadId, turnId, model: state.model,
4480
+ errorType: classifyError(new Error(turn.error.message)),
4481
+ message: formatApprovalLogSummary(turn.error.message),
4482
+ lastReportedUsage: state.tokenUsage?.last,
4483
+ })}`);
4415
4484
  if (isRetryableError(new Error(turn.error.message))) {
4416
4485
  throw new Error(turn.error.message);
4417
4486
  }
4418
- yield { type: 'error', error: turn.error.message, errorType: 'unknown' };
4487
+ yield { type: 'error', error: turn.error.message, errorType: toAgentErrorType(turn.error.message) };
4419
4488
  }
4420
4489
  yield this.mapAppServerTurnComplete(turn, state);
4421
4490
  break;
4422
4491
  }
4423
4492
  case 'error': {
4424
- if (!params.message) {
4425
- // SSE idle timeout reconnect — not a real task error, suppress
4426
- logger.debug(`[CodexRunner] app-server SSE reconnect (no message)`);
4493
+ // Current app-server uses error.message; older builds used message.
4494
+ const message = typeof params.error?.message === 'string' ? params.error.message : params.message;
4495
+ if (typeof message !== 'string' || !message.trim()) {
4496
+ logger.debug('[CodexRunner] app-server error notification without a diagnostic message');
4427
4497
  break;
4428
4498
  }
4429
- if (isRetryableError(new Error(params.message))) {
4430
- throw new Error(params.message);
4499
+ logger.warn(`[CodexRunner] error notification: ${JSON.stringify({
4500
+ sessionId, threadId: state.threadId, turnId: state.turnId, model: state.model,
4501
+ willRetry: params.willRetry ?? null,
4502
+ errorType: classifyError(new Error(message)), message: formatApprovalLogSummary(message),
4503
+ })}`);
4504
+ // Codex owns retries within this turn. Throwing here closes our subscription
4505
+ // before its recovery can finish; runQuery must not submit the input again.
4506
+ if (params.willRetry === true) {
4507
+ yield {
4508
+ type: 'notice', severity: 'warn', subtype: 'codex-model-retry',
4509
+ message: isOutputTokenLimitError(new Error(message))
4510
+ ? '模型响应达到输出上限,Codex 正在当前轮内重试。'
4511
+ : 'Codex 正在当前轮内重试模型请求。',
4512
+ };
4513
+ break;
4431
4514
  }
4432
- yield { type: 'error', error: params.message, errorType: 'unknown' };
4515
+ if (isRetryableError(new Error(message)))
4516
+ throw new Error(message);
4517
+ yield { type: 'error', error: message, errorType: toAgentErrorType(message) };
4433
4518
  break;
4434
4519
  }
4435
4520
  }
@@ -4860,16 +4945,17 @@ export class CodexRunner {
4860
4945
  }
4861
4946
  mapAppServerTurnComplete(turn, state) {
4862
4947
  const status = turn.status || 'completed';
4948
+ const outputLimit = status === 'failed' && isOutputTokenLimitError(new Error(turn.error?.message ?? ''));
4863
4949
  const tokenUsage = this.mapCodexTokenUsage(state.tokenUsage?.last ?? turn.tokenUsage ?? turn.usage);
4864
4950
  const contextUsage = this.mapCodexContextUsage(turn.contextUsage ?? state.tokenUsage?.contextUsage ?? state.tokenUsage?.context);
4865
4951
  const terminalReason = status === 'completed'
4866
4952
  ? undefined
4867
4953
  : status === 'interrupted'
4868
4954
  ? 'aborted_streaming'
4869
- : status;
4955
+ : outputLimit ? 'max_output_tokens' : status;
4870
4956
  return {
4871
4957
  type: 'complete',
4872
- subtype: status === 'completed' ? 'success' : status,
4958
+ subtype: status === 'completed' ? 'success' : outputLimit ? 'max_output_tokens' : status,
4873
4959
  isError: status === 'failed',
4874
4960
  errors: turn.error?.message ? [turn.error.message] : undefined,
4875
4961
  terminalReason,
@@ -1,4 +1,6 @@
1
1
  import { getRunnerCatalog, catalogEfforts, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
+ // Ecagent/OpenAI API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
+ import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
2
4
  import fs from 'fs';
3
5
  import path from 'path';
4
6
  import { AgentHarness, JsonlSessionRepo, NodeExecutionEnv, } from '../../ecagent/dist/node.js';
@@ -26,13 +28,6 @@ const MAX_TOOL_OUTPUT = 200_000;
26
28
  const MAX_SCAN_FILES = 5_000;
27
29
  const MAX_SEARCH_RESULTS = 500;
28
30
  const DEFAULT_BASH_TIMEOUT_MS = 120_000;
29
- /**
30
- * 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
31
- * Ecagent/OpenAI API 不认识这些后缀,必须传入标准模型 ID
32
- */
33
- function stripContextWindowSuffix(modelId) {
34
- return modelId.replace(/\[(?:1m|200k)\]$/i, '');
35
- }
36
31
  class AsyncEventQueue {
37
32
  values = [];
38
33
  waiters = [];
@@ -468,26 +463,27 @@ function isWithin(root, candidate) {
468
463
  const relative = path.relative(root, candidate);
469
464
  return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
470
465
  }
471
- export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false) {
466
+ export function resolveEcagentToolPath(projectPath, candidate, forWrite = false, bootstrapAgentMdPath, fullaccess = false, personalMdPath) {
472
467
  const root = fs.realpathSync(projectPath);
473
468
  const resolved = path.resolve(root, candidate || '.');
474
469
  const allowedBootstrapFile = bootstrapAgentMdPath
475
470
  ? path.resolve(bootstrapAgentMdPath)
476
471
  : undefined;
477
- const isBootstrapAgentMd = allowedBootstrapFile === resolved;
478
- if (!fullaccess && !isWithin(root, resolved) && !isBootstrapAgentMd) {
472
+ const allowedPersonalFile = personalMdPath ? path.resolve(personalMdPath) : undefined;
473
+ const isManagedIdentityFile = resolved === allowedBootstrapFile || resolved === allowedPersonalFile;
474
+ if (!fullaccess && !isWithin(root, resolved) && !isManagedIdentityFile) {
479
475
  throw new Error(`Path is outside the project: ${candidate}`);
480
476
  }
481
- if (isBootstrapAgentMd) {
477
+ if (isManagedIdentityFile) {
482
478
  const parent = path.dirname(resolved);
483
479
  const canonicalParent = fs.realpathSync(parent);
484
480
  if (!isWithin(parent, canonicalParent) || !isWithin(canonicalParent, parent)) {
485
- throw new Error(`Bootstrap agent.md parent resolves outside its declared path: ${candidate}`);
481
+ throw new Error(`Managed identity file parent resolves outside its declared path: ${candidate}`);
486
482
  }
487
483
  if (fs.existsSync(resolved)) {
488
484
  const canonicalFile = fs.realpathSync(resolved);
489
485
  if (!isWithin(resolved, canonicalFile) || !isWithin(canonicalFile, resolved)) {
490
- throw new Error(`Bootstrap agent.md resolves outside its declared path: ${candidate}`);
486
+ throw new Error(`Managed identity file resolves outside its declared path: ${candidate}`);
491
487
  }
492
488
  }
493
489
  return resolved;
@@ -550,7 +546,7 @@ async function walkFiles(root, signal) {
550
546
  function textResult(text, details = {}) {
551
547
  return { content: [{ type: 'text', text: truncate(text) }], details };
552
548
  }
553
- function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false) {
549
+ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullaccess = false, personalMdPath) {
554
550
  const read = {
555
551
  name: 'Read', label: 'Read', description: 'Read a UTF-8 text file from the project.',
556
552
  parameters: Type.Object({
@@ -559,7 +555,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
559
555
  limit: Type.Optional(Type.Number({ minimum: 1 })),
560
556
  }),
561
557
  async execute(_id, params, signal) {
562
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess);
558
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath);
563
559
  const stat = await fs.promises.stat(file);
564
560
  if (stat.size > MAX_READ_BYTES)
565
561
  throw new Error(`File exceeds ${MAX_READ_BYTES} byte read limit`);
@@ -625,7 +621,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
625
621
  name: 'Write', label: 'Write', description: 'Create or completely overwrite a project file.',
626
622
  parameters: Type.Object({ file_path: Type.String(), content: Type.String() }), executionMode: 'sequential',
627
623
  async execute(_id, params, signal) {
628
- const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess);
624
+ const file = resolveEcagentToolPath(projectPath, params.file_path, true, bootstrapAgentMdPath, fullaccess, personalMdPath);
629
625
  await fs.promises.mkdir(path.dirname(file), { recursive: true });
630
626
  await fs.promises.writeFile(file, params.content, { encoding: 'utf8', signal });
631
627
  return textResult(`Wrote ${Buffer.byteLength(params.content)} bytes to ${path.relative(projectPath, file)}`, { file });
@@ -638,7 +634,7 @@ function createTools(env, projectPath, runtimeEnv, bootstrapAgentMdPath, fullacc
638
634
  replace_all: Type.Optional(Type.Boolean()),
639
635
  }), executionMode: 'sequential',
640
636
  async execute(_id, params, signal) {
641
- const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess);
637
+ const file = resolveEcagentToolPath(projectPath, params.file_path, false, bootstrapAgentMdPath, fullaccess, personalMdPath);
642
638
  const value = await fs.promises.readFile(file, { encoding: 'utf8', signal });
643
639
  if (!params.old_string)
644
640
  throw new Error('old_string must not be empty');
@@ -858,7 +854,7 @@ export class EcagentRunner {
858
854
  models,
859
855
  model,
860
856
  thinkingLevel: normalizeThinkingLevel(effort),
861
- tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess),
857
+ tools: createTools(nodeEnv, request.projectPath, request.runtimeEnv, request.modelOverride?.bootstrapAgentMdPath, fullaccess, request.modelOverride?.personalMdPath),
862
858
  systemPrompt: [DEFAULT_SYSTEM_PROMPT, request.systemPromptAppend].filter(Boolean).join('\n\n'),
863
859
  streamOptions: {
864
860
  timeoutMs: this.config.timeoutMs,
@@ -1,4 +1,6 @@
1
1
  import { getCatalog, assertCatalogSelectable } from '../core/model/model-catalog.js';
2
+ // Gemini API 不认识 Claude Code SDK 的窗口后缀,必须传入基础模型 ID。
3
+ import { stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
2
4
  /**
3
5
  * Gemini Agent Runner
4
6
  *
@@ -33,13 +35,6 @@ const ANSI_RE = /\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x1b\\|\x07)/g;
33
35
  function stripAnsi(text) {
34
36
  return text.replace(ANSI_RE, '');
35
37
  }
36
- /**
37
- * 规范化模型 ID:去掉 Claude Code SDK 专有的上下文窗口后缀 [1m]/[200k]
38
- * Gemini API 不认识这些后缀,必须传入标准模型 ID
39
- */
40
- function stripContextWindowSuffix(modelId) {
41
- return modelId.replace(/\[(?:1m|200k)\]$/i, '');
42
- }
43
38
  // ── MIME → 扩展名映射 ──
44
39
  const MIME_EXT = {
45
40
  'image/png': '.png',
@@ -147,6 +142,7 @@ const GEMINI_EC_COMMAND_PATTERNS = [
147
142
  // quoted arguments.
148
143
  String.raw `\x00"command":"[ ]*ec[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))*"\x00`,
149
144
  ];
145
+ const GEMINI_BOOTSTRAP_PERSONA_COMMAND_PATTERN = String.raw `^\{\x00"command":"[ ]*ec[ ]+agent[ ]+persona(?:[ ]+show|[ ]+--(?:text|file)[ ]+(?:[^"\x00;&|$()<>\x60\r\n\\]|\\(?:\\|"))+)[ ]*"\x00\}$`;
150
146
  export function resolveGeminiPermissionProfile(value, bootstrap = false) {
151
147
  if (value === 'fullaccess')
152
148
  return { mode: 'fullaccess', approvalMode: 'yolo' };
@@ -186,6 +182,9 @@ function geminiExactShellCommandPattern(command) {
186
182
  // is_background or additional_permissions during bootstrap.
187
183
  return `^\\{\\x00"command":"[ ]*${escapeGeminiArgsRegex(command)}[ ]*"\\x00\\}$`;
188
184
  }
185
+ function geminiManagedIdentityPathPattern(filePath) {
186
+ return geminiJsonFieldEqualsPattern(['file_path'], path.resolve(filePath));
187
+ }
189
188
  function appendGeminiPolicyRule(lines, toolName, decision, priority, argsPattern) {
190
189
  lines.push('[[rule]]');
191
190
  lines.push(`toolName = ${Array.isArray(toolName) ? JSON.stringify(toolName) : JSON.stringify(toolName)}`);
@@ -194,7 +193,7 @@ function appendGeminiPolicyRule(lines, toolName, decision, priority, argsPattern
194
193
  lines.push(`decision = "${decision}"`);
195
194
  lines.push(`priority = ${priority}`, '');
196
195
  }
197
- export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath) {
196
+ export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath, personalMdPath) {
198
197
  const lines = [];
199
198
  if (bootstrapAgentMdPath) {
200
199
  const bootstrap = normalizeGeminiBootstrapAgentMdPath(bootstrapAgentMdPath);
@@ -212,6 +211,12 @@ export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath) {
212
211
  appendGeminiPolicyRule(lines, GEMINI_BOOTSTRAP_FILE_TOOLS, 'allow', 990, writePathPattern);
213
212
  appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec aid agentmd put ${bootstrap.aid}`));
214
213
  appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, geminiExactShellCommandPattern(`ec agent ready ${bootstrap.aid}`));
214
+ appendGeminiPolicyRule(lines, 'run_shell_command', 'allow', 990, GEMINI_BOOTSTRAP_PERSONA_COMMAND_PATTERN);
215
+ if (personalMdPath) {
216
+ const personalPathPattern = geminiManagedIdentityPathPattern(personalMdPath);
217
+ appendGeminiPolicyRule(lines, 'read_file', 'allow', 989, personalPathPattern);
218
+ appendGeminiPolicyRule(lines, GEMINI_BOOTSTRAP_FILE_TOOLS, 'allow', 989, personalPathPattern);
219
+ }
215
220
  // These are Gemini's non-I/O task bookkeeping tools. They do not expose
216
221
  // project files, shell, network, MCP, or user-interaction capabilities.
217
222
  appendGeminiPolicyRule(lines, GEMINI_BOOTSTRAP_LIFECYCLE_TOOLS, 'allow', 950);
@@ -231,6 +236,11 @@ export function buildGeminiAdminPolicy(profile, bootstrapAgentMdPath) {
231
236
  for (const pattern of GEMINI_L_CLASS_PATTERNS) {
232
237
  appendGeminiPolicyRule(lines, GEMINI_L_CLASS_WRITE_TOOLS, 'deny', 997, pattern);
233
238
  }
239
+ if (personalMdPath) {
240
+ const personalPathPattern = geminiManagedIdentityPathPattern(personalMdPath);
241
+ appendGeminiPolicyRule(lines, 'read_file', 'allow', 989, personalPathPattern);
242
+ appendGeminiPolicyRule(lines, GEMINI_BOOTSTRAP_FILE_TOOLS, 'allow', 989, personalPathPattern);
243
+ }
234
244
  if (profile.mode === 'readonly') {
235
245
  appendGeminiPolicyRule(lines, GEMINI_READONLY_TOOLS, 'allow', 950);
236
246
  appendGeminiPolicyRule(lines, '*', 'deny', 900);
@@ -365,6 +375,9 @@ export class GeminiRunner {
365
375
  const bootstrapAgentMdPath = modelOverride?.bootstrapAgentMdPath
366
376
  ? normalizeGeminiBootstrapAgentMdPath(modelOverride.bootstrapAgentMdPath).filePath
367
377
  : undefined;
378
+ const personalMdPath = modelOverride?.personalMdPath
379
+ ? path.resolve(modelOverride.personalMdPath)
380
+ : undefined;
368
381
  const permissionProfile = resolveGeminiPermissionProfile(requestedPermissionMode, !!bootstrapAgentMdPath);
369
382
  const callModel = modelOverride?.model || this.model;
370
383
  const managedTaskTmpDir = getManagedTaskTempDir(runtimeEnv);
@@ -382,6 +395,7 @@ export class GeminiRunner {
382
395
  ...(runtimeDir ? [runtimeDir] : []),
383
396
  ...(runtimeLockDir ? [runtimeLockDir] : []),
384
397
  ...(bootstrapAgentMdPath ? [bootstrapAgentMdPath] : []),
398
+ ...(personalMdPath ? [personalMdPath] : []),
385
399
  ],
386
400
  });
387
401
  if (executionPermissionMode !== 'fullaccess' && !sandboxProbe && workspaceContainsHClassPaths(projectPath)) {
@@ -410,7 +424,7 @@ export class GeminiRunner {
410
424
  }
411
425
  else {
412
426
  const policyPath = path.join(managedTmpDir, `evolcore-gemini-permission-${crypto.randomUUID()}.toml`);
413
- fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile, bootstrapAgentMdPath), { mode: 0o600, flag: 'wx' });
427
+ fs.writeFileSync(policyPath, buildGeminiAdminPolicy(permissionProfile, bootstrapAgentMdPath, personalMdPath), { mode: 0o600, flag: 'wx' });
414
428
  tempFiles.push(policyPath);
415
429
  args.push(...buildGeminiPermissionArgs(permissionProfile, policyPath, !!sandboxProbe));
416
430
  }
@@ -452,6 +466,7 @@ export class GeminiRunner {
452
466
  ...(runtimeDir ? [runtimeDir] : []),
453
467
  ...(runtimeLockDir ? [runtimeLockDir] : []),
454
468
  ...(bootstrapAgentMdPath ? [bootstrapAgentMdPath] : []),
469
+ ...(personalMdPath ? [personalMdPath] : []),
455
470
  ],
456
471
  readonlyPaths: tempFiles,
457
472
  });
@@ -1,3 +1,4 @@
1
+ import { parseModelId, stripContextWindowSuffix } from '../core/model/context-window-suffix.js';
1
2
  export const BASEAGENT_RUNNER_UNAVAILABLE = 'BASEAGENT_RUNNER_UNAVAILABLE';
2
3
  export class BaseagentRunnerUnavailableError extends Error {
3
4
  evolagentName;
@@ -69,20 +70,22 @@ const CLAUDE_CONTEXT_MODEL_ALIASES = ['opus', 'sonnet', 'haiku'];
69
70
  export function isClaudeContextUsageModel(model) {
70
71
  if (!model)
71
72
  return false;
72
- const baseModel = model.replace(/\[1m\]$/i, '');
73
+ const baseModel = stripContextWindowSuffix(model);
73
74
  return /^claude-/i.test(baseModel) || CLAUDE_CONTEXT_MODEL_ALIASES.includes(baseModel);
74
75
  }
75
76
  export function isOneMillionContextModel(model) {
76
77
  if (!model)
77
78
  return false;
79
+ // 显式后缀是网关给出的权威窗口,优先于本地前缀表:`claude-opus-5[200k]`
80
+ // 即使基础 ID 在 1M 名单里,也必须按 200k 处理。
81
+ const { base, contextWindow } = parseModelId(model);
82
+ if (contextWindow !== undefined)
83
+ return contextWindow >= 1_000_000;
78
84
  if (ONE_M_CONTEXT_MODEL_ALIASES.includes(model))
79
85
  return true;
80
- if (/\[1m\]$/i.test(model))
86
+ if (/deepseek-v4/i.test(base))
81
87
  return true;
82
- if (/deepseek-v4/i.test(model))
83
- return true;
84
- const baseModel = model.replace(/\[1m\]$/i, '');
85
- return ONE_M_CONTEXT_MODEL_PREFIXES.some(prefix => baseModel === prefix || baseModel.startsWith(`${prefix}-`));
88
+ return ONE_M_CONTEXT_MODEL_PREFIXES.some(prefix => base === prefix || base.startsWith(`${prefix}-`));
86
89
  }
87
90
  /**
88
91
  * EvolCore's conservative local context-window catalog: 1M models = 1,000,000;
@@ -94,17 +97,22 @@ export function realContextWindowForModel(model) {
94
97
  return isOneMillionContextModel(model) ? 1000000 : 200000;
95
98
  }
96
99
  /**
97
- * The SDK's compaction threshold must leave room for the next model turn and
98
- * its tool results. Keep a 10k reserve for 200k-class windows and retain the
99
- * historical 350k cap for large-context models.
100
+ * EvolCore auto-compact threshold: fixed at 650k tokens regardless of model
101
+ * context window. Large-window models (1M) compress at 650k; small-window
102
+ * models (200k/128k) never reach this threshold and rely on the SDK's own
103
+ * built-in compaction instead.
100
104
  */
101
- export function autoCompactWindowForContextWindow(contextWindow) {
102
- if (!contextWindow || contextWindow <= 0)
103
- return undefined;
104
- const reserve = Math.min(10_000, Math.floor(contextWindow * 0.05));
105
- return Math.min(350_000, Math.max(1, contextWindow - reserve));
105
+ export function autoCompactWindowForModel(_model) {
106
+ return 650_000;
106
107
  }
107
- /** Auto-compact threshold derived from the configured model context window. */
108
- export function autoCompactWindowForModel(model) {
109
- return autoCompactWindowForContextWindow(realContextWindowForModel(model)) ?? 350_000;
108
+ /**
109
+ * Threshold passed to the Claude Code SDK's autoCompactWindow option.
110
+ * Kept window-aware so the SDK's own built-in compaction continues to fire
111
+ * at the right point for small-context models (200k/128k), where EvolCore's
112
+ * fixed 650k threshold would never trigger.
113
+ */
114
+ export function sdkAutoCompactWindowForModel(model) {
115
+ const contextWindow = realContextWindowForModel(model);
116
+ const reserve = Math.min(10_000, Math.floor(contextWindow * 0.05));
117
+ return Math.min(650_000, Math.max(1, contextWindow - reserve));
110
118
  }