evolcore 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +69 -115
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +135 -17
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +486 -68
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +410 -54
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +253 -46
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
|
@@ -5,7 +5,7 @@ import { resolveAnthropicConfig } from './baseagent.js';
|
|
|
5
5
|
import { buildModelRequestHeaders } from './request-identity.js';
|
|
6
6
|
import { DEFAULT_PERMISSION_MODE } from '../types.js';
|
|
7
7
|
import { renderActionAsText } from '../core/interaction-router.js';
|
|
8
|
-
import { buildEnvelope, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
8
|
+
import { buildEnvelope, isInteractionSendAccepted, sendInteractionPayload } from '../core/message/message-utils.js';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import fs from 'fs';
|
|
11
11
|
import os from 'os';
|
|
@@ -21,12 +21,13 @@ import { getPackageRoot, resolvePaths } from '../paths.js';
|
|
|
21
21
|
import { resolveEffective } from '../config/config-manager.js';
|
|
22
22
|
import { sanitizeSessionTitle } from '../core/session/session-title.js';
|
|
23
23
|
import { resolveClaudeCapabilityRunOptionsForProject } from '../core/capability/capability-manager.js';
|
|
24
|
-
import { normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
25
|
-
import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/protected-paths.js';
|
|
26
|
-
import { buildHClassGuardCommand, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, recordSandboxInitializationFailure, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
|
|
24
|
+
import { normalizeExecutionPermissionMode, normalizePermissionMode, resolvePhaseOneExecutionSandbox } from '../core/permission/mode.js';
|
|
25
|
+
import { buildClaudeProtectedFilesystem, containsHClassReference, containsLClassReference, isHClassPath, isLClassPath, isSameOrDescendant, resolveProtectedCandidate, } from '../core/permission/protected-paths.js';
|
|
26
|
+
import { buildHClassGuardCommand, collectSandboxGuardDiagnostics, createSandboxInitializationError, getActiveSandboxInitializationFailure, isSandboxInitializationFailure, recordSandboxInitializationFailure, SANDBOX_INITIALIZATION_COOLDOWN_MS, SANDBOX_INITIALIZATION_FAILED, ensureClaudeGitWorktreeConfig, prependExecutableDirectory, resolveBubblewrapPath, summarizeBubblewrapArgsForLog, summarizeSandboxGuardDiagnostics, shouldFailIfClaudeSandboxUnavailable, shouldLogSandboxCircuitNotice, } from '../core/permission/sandbox-runtime.js';
|
|
27
27
|
import { buildClaudeUnixSocketAllowlist } from '../core/permission/unix-socket-policy.js';
|
|
28
28
|
import { auditToolInfrastructureFailure, auditToolPreflightDenial } from '../core/auth/authorization-audit.js';
|
|
29
|
-
import { isManagedSessionRuntimeDir } from '../cli/task-context.js';
|
|
29
|
+
import { getManagedTaskTempDir, isManagedSessionRuntimeDir } from '../cli/task-context.js';
|
|
30
|
+
import { hasTrustedFullAccessContext } from './runner-types.js';
|
|
30
31
|
import { contextTokensForUsage, usageForContext, isClaudeContextUsageModel, isOneMillionContextModel, realContextWindowForModel, autoCompactWindowForModel } from './runner-types.js';
|
|
31
32
|
export { hasCompact, hasModelSwitcher, hasPermissionController } from './runner-types.js';
|
|
32
33
|
// Built-in tools execute inside the Claude runtime and are covered by the
|
|
@@ -65,10 +66,18 @@ const CLAUDE_BUILTIN_TOOLS = new Set([
|
|
|
65
66
|
'Write',
|
|
66
67
|
]);
|
|
67
68
|
function spawnClaudeWithHClassGuard(options, root, onStderr) {
|
|
69
|
+
const preflight = collectSandboxGuardDiagnostics(root);
|
|
70
|
+
logger.info(`[ClaudeSandbox] H-class guard preflight ${JSON.stringify(summarizeSandboxGuardDiagnostics(preflight))}`);
|
|
68
71
|
const guarded = buildHClassGuardCommand(options.command, options.args, root);
|
|
69
72
|
if (!guarded) {
|
|
70
73
|
throw new Error('Claude owner bypass 缺少 EvolCore H 类路径隔离运行时,已拒绝启动');
|
|
71
74
|
}
|
|
75
|
+
const launch = summarizeBubblewrapArgsForLog(guarded.args);
|
|
76
|
+
const postBuild = collectSandboxGuardDiagnostics(root);
|
|
77
|
+
logger.info(`[ClaudeSandbox] H-class guard launch ${JSON.stringify({
|
|
78
|
+
...launch,
|
|
79
|
+
diagnostics: summarizeSandboxGuardDiagnostics(postBuild),
|
|
80
|
+
})}`);
|
|
72
81
|
const child = spawn(guarded.command, guarded.args, {
|
|
73
82
|
cwd: options.cwd,
|
|
74
83
|
env: options.env,
|
|
@@ -111,23 +120,6 @@ function stableClaudePermissionValue(value, depth = 0) {
|
|
|
111
120
|
.sort()
|
|
112
121
|
.map(key => [key, stableClaudePermissionValue(record[key], depth + 1)]));
|
|
113
122
|
}
|
|
114
|
-
function collectClaudePermissionStrings(value, output = []) {
|
|
115
|
-
if (typeof value === 'string') {
|
|
116
|
-
output.push(value);
|
|
117
|
-
return output;
|
|
118
|
-
}
|
|
119
|
-
if (Array.isArray(value)) {
|
|
120
|
-
for (const entry of value)
|
|
121
|
-
collectClaudePermissionStrings(entry, output);
|
|
122
|
-
return output;
|
|
123
|
-
}
|
|
124
|
-
if (!value || typeof value !== 'object')
|
|
125
|
-
return output;
|
|
126
|
-
for (const entry of Object.values(value)) {
|
|
127
|
-
collectClaudePermissionStrings(entry, output);
|
|
128
|
-
}
|
|
129
|
-
return output;
|
|
130
|
-
}
|
|
131
123
|
function inspectClaudePermissionExpansion(input, options, projectPath) {
|
|
132
124
|
const blockedPath = typeof options.blockedPath === 'string' && options.blockedPath.length > 0
|
|
133
125
|
? options.blockedPath
|
|
@@ -161,9 +153,6 @@ function inspectClaudePermissionExpansion(input, options, projectPath) {
|
|
|
161
153
|
break;
|
|
162
154
|
}
|
|
163
155
|
}
|
|
164
|
-
if (!protectedReason && collectClaudePermissionStrings(rawSuggestions).some(containsHClassReference)) {
|
|
165
|
-
protectedReason = '🔒 Claude SDK 权限规则涉及 EvolCore H 类受保护路径,已拒绝';
|
|
166
|
-
}
|
|
167
156
|
const hasDirectoryExpansion = rawSuggestions.some(entry => {
|
|
168
157
|
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
169
158
|
return false;
|
|
@@ -600,7 +589,7 @@ class MessageStream {
|
|
|
600
589
|
}
|
|
601
590
|
export class AgentRunner {
|
|
602
591
|
name = 'claude';
|
|
603
|
-
capabilities = { clear: true, compact: true, fork: true, forkAtTurn: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
|
|
592
|
+
capabilities = { fullaccess: true, clear: true, compact: true, fork: true, forkAtTurn: true, askUserQuestion: true, planApproval: true, fileRewind: 'checkpoint' };
|
|
604
593
|
apiKey;
|
|
605
594
|
model;
|
|
606
595
|
effort;
|
|
@@ -609,6 +598,8 @@ export class AgentRunner {
|
|
|
609
598
|
customHeaders;
|
|
610
599
|
customQueryParams;
|
|
611
600
|
config;
|
|
601
|
+
agentAid;
|
|
602
|
+
agentConfig;
|
|
612
603
|
activeSessions = new Map();
|
|
613
604
|
activeStreams = new Map();
|
|
614
605
|
activeMessageStreams = new Map();
|
|
@@ -636,6 +627,7 @@ export class AgentRunner {
|
|
|
636
627
|
reason: detail.message.slice(0, 512),
|
|
637
628
|
sessionId,
|
|
638
629
|
agentAid: context?.selfAid,
|
|
630
|
+
agentName: context?.agentName,
|
|
639
631
|
permissionMode,
|
|
640
632
|
role: context?.role,
|
|
641
633
|
callId: inputId,
|
|
@@ -654,6 +646,7 @@ export class AgentRunner {
|
|
|
654
646
|
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
655
647
|
decisionSource: 'infrastructure',
|
|
656
648
|
agentAid: context?.selfAid,
|
|
649
|
+
agentName: context?.agentName,
|
|
657
650
|
permissionMode: permissionMode,
|
|
658
651
|
summary: detail.message.slice(0, 512),
|
|
659
652
|
effect: 'operation_skipped',
|
|
@@ -687,12 +680,17 @@ export class AgentRunner {
|
|
|
687
680
|
this.cleanupSandboxSettingsFile(sessionId, filePath);
|
|
688
681
|
}
|
|
689
682
|
}
|
|
690
|
-
constructor(apiKey, model, onSessionIdUpdate, baseUrl, config) {
|
|
683
|
+
constructor(apiKey, model, onSessionIdUpdate, baseUrl, config, runtime) {
|
|
684
|
+
// Compatibility for legacy direct callers that still pass the removed
|
|
685
|
+
// internal fields in a synthetic config. AgentLoader uses runtime binding.
|
|
686
|
+
const legacy = config?.agents?.claude;
|
|
691
687
|
this.apiKey = apiKey;
|
|
692
688
|
this.model = model || 'sonnet';
|
|
693
689
|
this.effort = undefined;
|
|
694
690
|
this.baseUrl = baseUrl;
|
|
695
691
|
this.config = config;
|
|
692
|
+
this.agentAid = runtime?.agentAid ?? legacy?.['evolcoreAgentAid'];
|
|
693
|
+
this.agentConfig = runtime?.agentConfig ?? legacy?.['evolcoreAgentConfig'];
|
|
696
694
|
this.onSessionIdUpdate = onSessionIdUpdate;
|
|
697
695
|
if (config) {
|
|
698
696
|
const anthropic = resolveAnthropicConfig(config);
|
|
@@ -700,7 +698,7 @@ export class AgentRunner {
|
|
|
700
698
|
this.customHeaders = buildModelRequestHeaders({
|
|
701
699
|
baseagent: 'claude',
|
|
702
700
|
baseUrl: anthropic.baseUrl,
|
|
703
|
-
agentAid:
|
|
701
|
+
agentAid: this.agentAid,
|
|
704
702
|
configuredHeaders: anthropic.headers,
|
|
705
703
|
});
|
|
706
704
|
this.customQueryParams = anthropic.queryParams;
|
|
@@ -744,11 +742,10 @@ export class AgentRunner {
|
|
|
744
742
|
return entry?.cache;
|
|
745
743
|
}
|
|
746
744
|
async resolveCapabilityRunOptions(projectPath) {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if (claudeConfig?.evolcoreAgentAid) {
|
|
745
|
+
let agentConfig = this.agentConfig;
|
|
746
|
+
if (this.agentAid) {
|
|
750
747
|
try {
|
|
751
|
-
agentConfig = resolveEffective({ self:
|
|
748
|
+
agentConfig = resolveEffective({ self: this.agentAid }, { cache: true, expand: true });
|
|
752
749
|
}
|
|
753
750
|
catch { }
|
|
754
751
|
}
|
|
@@ -826,14 +823,16 @@ export class AgentRunner {
|
|
|
826
823
|
this.permissionContexts.set(sessionId, context);
|
|
827
824
|
}
|
|
828
825
|
toSdkPermissionMode(mode) {
|
|
826
|
+
if (mode === 'fullaccess')
|
|
827
|
+
return 'bypassPermissions';
|
|
829
828
|
const normalized = normalizePermissionMode(mode ?? this.permissionMode);
|
|
830
829
|
// Public permission policy is enforced by PreToolUse. Keeping the SDK in
|
|
831
830
|
// default mode preserves canUseTool reachability for request mode.
|
|
832
831
|
return normalized.workflow === 'plan' ? 'plan' : 'default';
|
|
833
832
|
}
|
|
834
833
|
// ── Compactable 接口 ──
|
|
835
|
-
async compact(sessionId, agentSessionId, projectPath) {
|
|
836
|
-
return this.compactSession(sessionId, agentSessionId, projectPath);
|
|
834
|
+
async compact(sessionId, agentSessionId, projectPath, modelOverride) {
|
|
835
|
+
return this.compactSession(sessionId, agentSessionId, projectPath, modelOverride);
|
|
837
836
|
}
|
|
838
837
|
syncFromUserSettings() {
|
|
839
838
|
try {
|
|
@@ -972,7 +971,7 @@ export class AgentRunner {
|
|
|
972
971
|
: '回复 /ask 1,或 /ask <自定义内容>';
|
|
973
972
|
const fallbackText = `💬 ${q.header || q.question}\n${q.header ? q.question + '\n' : ''}${optionLines}\n\n${answerHint}`;
|
|
974
973
|
const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
|
|
975
|
-
cardSent =
|
|
974
|
+
cardSent = isInteractionSendAccepted(result);
|
|
976
975
|
}
|
|
977
976
|
catch (err) {
|
|
978
977
|
logger.warn(`[AgentRunner] AskUserQuestion card send failed for q${i}:`, err);
|
|
@@ -1393,7 +1392,7 @@ export class AgentRunner {
|
|
|
1393
1392
|
});
|
|
1394
1393
|
const fallbackText = '📋 计划审批:AI 已完成规划,等待审批。\n回复 /ask 1 批准 / /ask 2 拒绝';
|
|
1395
1394
|
const result = await sendInteractionPayload(permCtx.adapter, envelope, interaction, fallbackText, permCtx.replyContext);
|
|
1396
|
-
cardSent =
|
|
1395
|
+
cardSent = isInteractionSendAccepted(result);
|
|
1397
1396
|
}
|
|
1398
1397
|
catch (err) {
|
|
1399
1398
|
logger.warn('[AgentRunner] ExitPlanMode card send failed:', err);
|
|
@@ -1441,7 +1440,7 @@ export class AgentRunner {
|
|
|
1441
1440
|
* SDK 原始事件 → 标准 AgentEvent 转换
|
|
1442
1441
|
* 所有 SDK 特有的事件类型引用封装在此方法内
|
|
1443
1442
|
*/
|
|
1444
|
-
async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode) {
|
|
1443
|
+
async *transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, isResuming = false, sandboxSettingsFile, permissionMode, turn) {
|
|
1445
1444
|
let lastSessionId;
|
|
1446
1445
|
let hasTurnActivity = false;
|
|
1447
1446
|
let ignoredPreTurnResult = false;
|
|
@@ -1458,6 +1457,7 @@ export class AgentRunner {
|
|
|
1458
1457
|
let turnCount = 0;
|
|
1459
1458
|
const seenMessageIds = new Set();
|
|
1460
1459
|
let lastModelCall;
|
|
1460
|
+
let lastAssistantWasSynthetic = false;
|
|
1461
1461
|
let lastAssistantUuid;
|
|
1462
1462
|
// 流式收集各次大模型调用(fallback:SDK iterations 为空时使用)
|
|
1463
1463
|
const collectedCalls = [];
|
|
@@ -1466,8 +1466,15 @@ export class AgentRunner {
|
|
|
1466
1466
|
// 提取 session_id(任意 SDK 事件都可能携带)
|
|
1467
1467
|
if (event.session_id && event.session_id !== lastSessionId) {
|
|
1468
1468
|
lastSessionId = event.session_id;
|
|
1469
|
-
this.
|
|
1470
|
-
|
|
1469
|
+
if (await this.acceptDiscoveredSessionId(sessionId, event.session_id, turn)) {
|
|
1470
|
+
yield { type: 'session_id', sessionId: event.session_id };
|
|
1471
|
+
}
|
|
1472
|
+
else {
|
|
1473
|
+
logger.warn(`[AgentRunner] Rejected stale backend discovery: sessionId=${sessionId} backend=${event.session_id} turn=${turn?.turnId ?? 'legacy'}`);
|
|
1474
|
+
const error = new Error('backend discovery rejected by active turn lease');
|
|
1475
|
+
error.code = 'TOPIC_BACKEND_DISCOVERY_REJECTED';
|
|
1476
|
+
throw error;
|
|
1477
|
+
}
|
|
1471
1478
|
}
|
|
1472
1479
|
if (event.type === 'user' && event.uuid === inputId) {
|
|
1473
1480
|
hasTurnActivity = true;
|
|
@@ -1563,7 +1570,9 @@ export class AgentRunner {
|
|
|
1563
1570
|
const streamEvent = event.event;
|
|
1564
1571
|
if (streamEvent?.type === 'message_start' && streamEvent.message?.usage) {
|
|
1565
1572
|
hasTurnActivity = true;
|
|
1573
|
+
lastAssistantWasSynthetic = streamEvent.message.model === '<synthetic>';
|
|
1566
1574
|
lastModelCall = {
|
|
1575
|
+
messageId: streamEvent.message.id,
|
|
1567
1576
|
uuid: event.uuid,
|
|
1568
1577
|
model: streamEvent.message.model,
|
|
1569
1578
|
tokenUsage: streamEvent.message.usage,
|
|
@@ -1573,6 +1582,7 @@ export class AgentRunner {
|
|
|
1573
1582
|
call_index: collectedCalls.length,
|
|
1574
1583
|
model: streamEvent.message.model ?? callModel ?? this.model,
|
|
1575
1584
|
request_id: event.request_id,
|
|
1585
|
+
message_id: streamEvent.message.id,
|
|
1576
1586
|
tokenUsage: { ...streamEvent.message.usage },
|
|
1577
1587
|
});
|
|
1578
1588
|
}
|
|
@@ -1625,27 +1635,40 @@ export class AgentRunner {
|
|
|
1625
1635
|
yield { type: 'state_changed', state: event.state };
|
|
1626
1636
|
}
|
|
1627
1637
|
// assistant: 提取 tool_use 和文本(仅无 text_delta 时提取文本)
|
|
1628
|
-
if (event.type === 'assistant'
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
if (
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
+
if (event.type === 'assistant') {
|
|
1639
|
+
const isSyntheticAssistant = event.message?.model === '<synthetic>'
|
|
1640
|
+
|| (typeof event.error === 'string' && event.isApiErrorMessage === true);
|
|
1641
|
+
lastAssistantWasSynthetic = isSyntheticAssistant;
|
|
1642
|
+
if (event.message && (event.message.usage || isSyntheticAssistant)) {
|
|
1643
|
+
// A message id is stable across the partial stream and the final
|
|
1644
|
+
// assistant event. Never merge a synthetic/API-error assistant into
|
|
1645
|
+
// the preceding real call, even if an upstream adapter reuses an id
|
|
1646
|
+
// or omits the usage object on the error frame.
|
|
1647
|
+
const sameModelCall = !isSyntheticAssistant
|
|
1648
|
+
&& typeof event.message.id === 'string'
|
|
1649
|
+
&& event.message.id === lastModelCall?.messageId;
|
|
1638
1650
|
lastModelCall = {
|
|
1639
|
-
...lastModelCall,
|
|
1651
|
+
...(sameModelCall ? lastModelCall : undefined),
|
|
1640
1652
|
messageId: event.message.id,
|
|
1653
|
+
uuid: event.uuid,
|
|
1641
1654
|
requestId: event.request_id,
|
|
1642
1655
|
model: event.message.model,
|
|
1643
|
-
tokenUsage: {
|
|
1656
|
+
tokenUsage: sameModelCall ? {
|
|
1644
1657
|
...event.message.usage,
|
|
1645
1658
|
...(lastModelCall?.tokenUsage ?? {}),
|
|
1646
|
-
},
|
|
1659
|
+
} : { ...(event.message.usage ?? {}) },
|
|
1647
1660
|
};
|
|
1648
1661
|
}
|
|
1662
|
+
if (!event.message?.content)
|
|
1663
|
+
continue;
|
|
1664
|
+
hasTurnActivity = true;
|
|
1665
|
+
lastAssistantUuid = event.uuid ?? lastAssistantUuid;
|
|
1666
|
+
const msgId = event.message.id;
|
|
1667
|
+
if (!msgId || !seenMessageIds.has(msgId)) {
|
|
1668
|
+
if (msgId)
|
|
1669
|
+
seenMessageIds.add(msgId);
|
|
1670
|
+
turnCount++;
|
|
1671
|
+
}
|
|
1649
1672
|
// 统计本轮 base agent 全部输出字符数(text + tool_use input)
|
|
1650
1673
|
let turnOutputChars = 0;
|
|
1651
1674
|
for (const content of event.message.content) {
|
|
@@ -1720,6 +1743,18 @@ export class AgentRunner {
|
|
|
1720
1743
|
// 非 Claude(DeepSeek/OpenAI 兼容):cache_read 是服务端 KV cache 不占上下文窗口,
|
|
1721
1744
|
// input_tokens 本身就是完整的上下文输入量。
|
|
1722
1745
|
const u = event.usage;
|
|
1746
|
+
// Claude Code represents transport/API failures as a fresh
|
|
1747
|
+
// `<synthetic>` assistant message. Its usage describes the failed
|
|
1748
|
+
// terminal request (normally zero); event.usage remains the aggregate
|
|
1749
|
+
// of earlier successful calls in the query and must not be presented
|
|
1750
|
+
// as the failed request's context usage.
|
|
1751
|
+
const isSyntheticError = event.is_error === true
|
|
1752
|
+
&& (lastAssistantWasSynthetic || lastModelCall?.model === '<synthetic>');
|
|
1753
|
+
const syntheticUsage = lastModelCall?.tokenUsage ?? {
|
|
1754
|
+
input_tokens: 0,
|
|
1755
|
+
output_tokens: 0,
|
|
1756
|
+
};
|
|
1757
|
+
const terminalUsage = isSyntheticError ? syntheticUsage : u;
|
|
1723
1758
|
const effectiveModel = callModel ?? this.model;
|
|
1724
1759
|
const isClaudeModel = isClaudeContextUsageModel(effectiveModel);
|
|
1725
1760
|
const totalTokens = contextTokensForUsage(u, !!isClaudeModel);
|
|
@@ -1762,7 +1797,11 @@ export class AgentRunner {
|
|
|
1762
1797
|
// 组装 modelCalls:优先 SDK iterations,fallback 流式收集,兜底降级单行。
|
|
1763
1798
|
const callModel_ = callModel ?? this.model;
|
|
1764
1799
|
let modelCalls;
|
|
1765
|
-
const iterArr =
|
|
1800
|
+
const iterArr = !isSyntheticError
|
|
1801
|
+
&& Array.isArray(u?.iterations)
|
|
1802
|
+
&& u.iterations.length > 0
|
|
1803
|
+
? u.iterations
|
|
1804
|
+
: null;
|
|
1766
1805
|
if (iterArr) {
|
|
1767
1806
|
modelCalls = iterArr.map((it, i) => ({
|
|
1768
1807
|
call_index: i, model: callModel_, tokenUsage: it, contextUsage: contextUsageForCall(it),
|
|
@@ -1774,16 +1813,19 @@ export class AgentRunner {
|
|
|
1774
1813
|
contextUsage: contextUsageForCall(call.tokenUsage),
|
|
1775
1814
|
}));
|
|
1776
1815
|
}
|
|
1777
|
-
else if (u) {
|
|
1816
|
+
else if (u && !isSyntheticError) {
|
|
1778
1817
|
// 降级:无逐次数据,写一条累计行
|
|
1779
1818
|
modelCalls = [{ call_index: 0, model: callModel_, tokenUsage: u, contextUsage: contextUsageForCall(u), degraded: true }];
|
|
1780
1819
|
}
|
|
1781
1820
|
// `event.usage` is aggregate query usage when the SDK made multiple
|
|
1782
1821
|
// model calls. The top-level context field must describe a single
|
|
1783
|
-
// current request
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1822
|
+
// current request. For normal results aggregate usage remains available
|
|
1823
|
+
// in tokenUsage; synthetic errors use their own terminal usage instead.
|
|
1824
|
+
const contextUsage = isSyntheticError
|
|
1825
|
+
? lastModelCall?.contextUsage
|
|
1826
|
+
: lastModelCall?.contextUsage
|
|
1827
|
+
?? modelCalls?.at(-1)?.contextUsage
|
|
1828
|
+
?? aggregateContextUsage;
|
|
1787
1829
|
const completeEvent = {
|
|
1788
1830
|
type: 'complete',
|
|
1789
1831
|
result: cleanResult,
|
|
@@ -1796,7 +1838,7 @@ export class AgentRunner {
|
|
|
1796
1838
|
terminalReason: event.terminal_reason,
|
|
1797
1839
|
sessionTitle: event.session_title,
|
|
1798
1840
|
numTurns: event.num_turns,
|
|
1799
|
-
tokenUsage:
|
|
1841
|
+
tokenUsage: terminalUsage,
|
|
1800
1842
|
contextUsage,
|
|
1801
1843
|
lastModelCall,
|
|
1802
1844
|
modelCalls,
|
|
@@ -1871,6 +1913,7 @@ export class AgentRunner {
|
|
|
1871
1913
|
policyCode: 'claude_startup_arguments_too_long',
|
|
1872
1914
|
decisionSource: 'infrastructure',
|
|
1873
1915
|
agentAid: this.permissionContexts.get(sessionId)?.selfAid,
|
|
1916
|
+
agentName: this.permissionContexts.get(sessionId)?.agentName,
|
|
1874
1917
|
summary: detail.message.slice(0, 512),
|
|
1875
1918
|
effect: 'operation_skipped',
|
|
1876
1919
|
});
|
|
@@ -1904,6 +1947,9 @@ export class AgentRunner {
|
|
|
1904
1947
|
}
|
|
1905
1948
|
}
|
|
1906
1949
|
async runQuery(sessionId, prompt, projectPath, initialClaudeSessionId, images, systemPromptAppend, sessionManager, modelOverride, runtimeEnv) {
|
|
1950
|
+
// Validate the daemon-injected session namespace before the SDK inherits
|
|
1951
|
+
// the process environment for any temporary state.
|
|
1952
|
+
getManagedTaskTempDir(runtimeEnv);
|
|
1907
1953
|
// 记录当前 evolcore session ID,用于 Agent ctl 环境变量注入
|
|
1908
1954
|
// 同步用户级配置到内存
|
|
1909
1955
|
this.syncFromUserSettings();
|
|
@@ -1922,8 +1968,22 @@ export class AgentRunner {
|
|
|
1922
1968
|
}
|
|
1923
1969
|
ensureDir(projectPath);
|
|
1924
1970
|
ensureDir(path.join(projectPath, '.claude'));
|
|
1925
|
-
|
|
1926
|
-
|
|
1971
|
+
const topicBinding = modelOverride?.backend?.kind === 'topic';
|
|
1972
|
+
if (topicBinding && (!modelOverride?.turn || modelOverride.turn.sessionId !== sessionId || !this.onSessionIdUpdate)) {
|
|
1973
|
+
const error = new Error('topic backend run requires an active TurnLease and binding callback');
|
|
1974
|
+
error.code = 'TOPIC_TURN_LEASE_REQUIRED';
|
|
1975
|
+
throw error;
|
|
1976
|
+
}
|
|
1977
|
+
const topicAgentSessionId = topicBinding ? modelOverride?.backend?.agentSessionId ?? null : null;
|
|
1978
|
+
const cachedAgentSessionId = topicBinding
|
|
1979
|
+
? (topicAgentSessionId && this.activeSessions.get(sessionId) === topicAgentSessionId
|
|
1980
|
+
? topicAgentSessionId
|
|
1981
|
+
: undefined)
|
|
1982
|
+
: this.activeSessions.get(sessionId);
|
|
1983
|
+
let agentSessionId = initialClaudeSessionId || cachedAgentSessionId;
|
|
1984
|
+
if (topicBinding && topicAgentSessionId !== agentSessionId) {
|
|
1985
|
+
agentSessionId = topicAgentSessionId || undefined;
|
|
1986
|
+
}
|
|
1927
1987
|
// 验证会话文件是否存在且有效(仅在有 agentSessionId 时)
|
|
1928
1988
|
if (agentSessionId) {
|
|
1929
1989
|
const homeDir = os.homedir();
|
|
@@ -1954,11 +2014,18 @@ export class AgentRunner {
|
|
|
1954
2014
|
}
|
|
1955
2015
|
}
|
|
1956
2016
|
if (!isValid) {
|
|
2017
|
+
if (topicBinding) {
|
|
2018
|
+
const error = new Error(`bound topic backend is unavailable: ${agentSessionId}`);
|
|
2019
|
+
error.code = 'TOPIC_BACKEND_UNAVAILABLE';
|
|
2020
|
+
throw error;
|
|
2021
|
+
}
|
|
1957
2022
|
logger.warn(`[AgentRunner] Invalid session file, starting new session`);
|
|
1958
2023
|
agentSessionId = undefined;
|
|
1959
2024
|
this.activeSessions.delete(sessionId);
|
|
1960
2025
|
if (this.onSessionIdUpdate) {
|
|
1961
|
-
this.onSessionIdUpdate(sessionId, '')
|
|
2026
|
+
void Promise.resolve(this.onSessionIdUpdate(sessionId, '')).catch(error => {
|
|
2027
|
+
logger.debug(`[AgentRunner] ignored empty session binding callback failure: ${error instanceof Error ? error.message : String(error)}`);
|
|
2028
|
+
});
|
|
1962
2029
|
}
|
|
1963
2030
|
}
|
|
1964
2031
|
}
|
|
@@ -1973,9 +2040,11 @@ export class AgentRunner {
|
|
|
1973
2040
|
// 缺省回落 agent 级 this.permissionMode。作为 per-call 入参(hook/canUseTool 闭包捕获),
|
|
1974
2041
|
// 不写实例字段,多对端并发互不污染(与 model/effort 同构)。
|
|
1975
2042
|
const requestedPermissionMode = modelOverride?.permissionMode || this.permissionMode;
|
|
1976
|
-
const
|
|
1977
|
-
const callPermissionMode = normalizedPermission.mode;
|
|
2043
|
+
const callPermissionMode = normalizeExecutionPermissionMode(requestedPermissionMode);
|
|
1978
2044
|
const runPermissionContext = this.permissionContexts.get(sessionId);
|
|
2045
|
+
if (callPermissionMode === 'fullaccess' && !hasTrustedFullAccessContext(runPermissionContext)) {
|
|
2046
|
+
throw new Error('Claude fullaccess requires a trusted per-call execution authorization');
|
|
2047
|
+
}
|
|
1979
2048
|
const executionSandbox = resolvePhaseOneExecutionSandbox(callPermissionMode, runPermissionContext?.role);
|
|
1980
2049
|
const inputId = modelOverride?.turn?.inputId ?? crypto.randomUUID();
|
|
1981
2050
|
if (executionSandbox.state !== 'off') {
|
|
@@ -1990,6 +2059,7 @@ export class AgentRunner {
|
|
|
1990
2059
|
reason: detail.message.slice(0, 512),
|
|
1991
2060
|
sessionId,
|
|
1992
2061
|
agentAid: context?.selfAid,
|
|
2062
|
+
agentName: context?.agentName,
|
|
1993
2063
|
permissionMode: callPermissionMode,
|
|
1994
2064
|
role: context?.role,
|
|
1995
2065
|
callId: inputId,
|
|
@@ -2008,6 +2078,7 @@ export class AgentRunner {
|
|
|
2008
2078
|
policyCode: SANDBOX_INITIALIZATION_FAILED,
|
|
2009
2079
|
decisionSource: 'infrastructure',
|
|
2010
2080
|
agentAid: context?.selfAid,
|
|
2081
|
+
agentName: context?.agentName,
|
|
2011
2082
|
permissionMode: callPermissionMode,
|
|
2012
2083
|
summary: detail.message.slice(0, 512),
|
|
2013
2084
|
effect: 'operation_skipped',
|
|
@@ -2054,6 +2125,7 @@ export class AgentRunner {
|
|
|
2054
2125
|
summary,
|
|
2055
2126
|
sessionId,
|
|
2056
2127
|
agentAid: ctx?.selfAid,
|
|
2128
|
+
agentName: ctx?.agentName,
|
|
2057
2129
|
permissionMode: callPermissionMode,
|
|
2058
2130
|
channel: ctx?.channel,
|
|
2059
2131
|
actorId: ctx?.userId,
|
|
@@ -2074,6 +2146,7 @@ export class AgentRunner {
|
|
|
2074
2146
|
policyCode: code,
|
|
2075
2147
|
decisionSource: 'policy',
|
|
2076
2148
|
agentAid: ctx?.selfAid,
|
|
2149
|
+
agentName: ctx?.agentName,
|
|
2077
2150
|
permissionMode: callPermissionMode,
|
|
2078
2151
|
summary,
|
|
2079
2152
|
effect: 'operation_skipped',
|
|
@@ -2122,6 +2195,14 @@ export class AgentRunner {
|
|
|
2122
2195
|
delete toolInput.pages;
|
|
2123
2196
|
}
|
|
2124
2197
|
let updatedInput = toolInput === originalInput ? undefined : toolInput;
|
|
2198
|
+
if (callPermissionMode === 'fullaccess') {
|
|
2199
|
+
if (!hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
|
|
2200
|
+
return hookDecision('deny', 'fullaccess execution authorization is no longer active', updatedInput);
|
|
2201
|
+
}
|
|
2202
|
+
return hookDecision('allow', undefined, toolName === 'Bash'
|
|
2203
|
+
? { ...toolInput, dangerouslyDisableSandbox: true }
|
|
2204
|
+
: updatedInput);
|
|
2205
|
+
}
|
|
2125
2206
|
const preparedSafeOutputCommand = toolName === 'Bash' && typeof toolInput.command === 'string'
|
|
2126
2207
|
? toolInput.command
|
|
2127
2208
|
: undefined;
|
|
@@ -2143,6 +2224,7 @@ export class AgentRunner {
|
|
|
2143
2224
|
: undefined;
|
|
2144
2225
|
const preflight = evaluateToolPreflight(toolName, toolInput, {
|
|
2145
2226
|
sessionId,
|
|
2227
|
+
managedTempDir: permCtx?.managedTempDir,
|
|
2146
2228
|
selfAid: permCtx?.selfAid,
|
|
2147
2229
|
channel: permCtx?.channel,
|
|
2148
2230
|
userId: permCtx?.userId,
|
|
@@ -2179,6 +2261,7 @@ export class AgentRunner {
|
|
|
2179
2261
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
2180
2262
|
const readonlyContext = {
|
|
2181
2263
|
sessionId,
|
|
2264
|
+
managedTempDir: permCtx?.managedTempDir,
|
|
2182
2265
|
channel: permCtx?.channel,
|
|
2183
2266
|
peerId: permCtx?.userId,
|
|
2184
2267
|
role: permCtx?.role,
|
|
@@ -2250,6 +2333,20 @@ export class AgentRunner {
|
|
|
2250
2333
|
if (toolName === 'ExitPlanMode') {
|
|
2251
2334
|
return await this.handleExitPlanMode(sessionId, input, options);
|
|
2252
2335
|
}
|
|
2336
|
+
if (callPermissionMode === 'fullaccess') {
|
|
2337
|
+
if (!hasTrustedFullAccessContext(this.permissionContexts.get(sessionId))) {
|
|
2338
|
+
return {
|
|
2339
|
+
behavior: 'deny',
|
|
2340
|
+
message: 'fullaccess execution authorization is no longer active',
|
|
2341
|
+
decisionClassification: 'user_reject',
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
return {
|
|
2345
|
+
behavior: 'allow',
|
|
2346
|
+
updatedInput: toolName === 'Bash' ? { ...input, dangerouslyDisableSandbox: true } : input,
|
|
2347
|
+
decisionClassification: 'user_permanent',
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2253
2350
|
if (toolName === 'Bash' && 'dangerouslyDisableSandbox' in input) {
|
|
2254
2351
|
input = { ...input };
|
|
2255
2352
|
delete input.dangerouslyDisableSandbox;
|
|
@@ -2267,6 +2364,7 @@ export class AgentRunner {
|
|
|
2267
2364
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
2268
2365
|
const preflight = evaluateToolPreflight(toolName, input, {
|
|
2269
2366
|
sessionId,
|
|
2367
|
+
managedTempDir: permCtx?.managedTempDir,
|
|
2270
2368
|
selfAid: permCtx?.selfAid,
|
|
2271
2369
|
channel: permCtx?.channel,
|
|
2272
2370
|
userId: permCtx?.userId,
|
|
@@ -2339,6 +2437,7 @@ export class AgentRunner {
|
|
|
2339
2437
|
const permCtx = this.permissionContexts.get(sessionId);
|
|
2340
2438
|
const readonlyContext = {
|
|
2341
2439
|
sessionId,
|
|
2440
|
+
managedTempDir: permCtx?.managedTempDir,
|
|
2342
2441
|
channel: permCtx?.channel,
|
|
2343
2442
|
peerId: permCtx?.userId,
|
|
2344
2443
|
role: permCtx?.role,
|
|
@@ -2403,7 +2502,9 @@ export class AgentRunner {
|
|
|
2403
2502
|
};
|
|
2404
2503
|
};
|
|
2405
2504
|
const useSettingSources = this.config?.agents?.claude?.useSettingSources !== false;
|
|
2406
|
-
const settingSources =
|
|
2505
|
+
const settingSources = callPermissionMode === 'fullaccess'
|
|
2506
|
+
? []
|
|
2507
|
+
: useSettingSources ? ['project', 'user'] : [];
|
|
2407
2508
|
const enableSummaries = this.config?.agents?.claude?.agentProgressSummaries !== false;
|
|
2408
2509
|
const excludeDynamic = this.config?.agents?.claude?.excludeDynamicSections === true;
|
|
2409
2510
|
// 公共 options(新旧模式共用)
|
|
@@ -2426,7 +2527,9 @@ export class AgentRunner {
|
|
|
2426
2527
|
}
|
|
2427
2528
|
const sdkModel = resolveSdkModel(callModel, this.baseUrl);
|
|
2428
2529
|
const capabilityOptions = await this.resolveCapabilityRunOptions(projectPath);
|
|
2429
|
-
const managedSettings =
|
|
2530
|
+
const managedSettings = callPermissionMode === 'fullaccess'
|
|
2531
|
+
? undefined
|
|
2532
|
+
: buildClaudeManagedLockdownSettings(capabilityOptions);
|
|
2430
2533
|
const sandboxOptions = executionSandbox.state === 'off'
|
|
2431
2534
|
? { enabled: false }
|
|
2432
2535
|
: await (async () => {
|
|
@@ -2466,9 +2569,11 @@ export class AgentRunner {
|
|
|
2466
2569
|
};
|
|
2467
2570
|
})();
|
|
2468
2571
|
if (executionSandbox.state === 'off') {
|
|
2469
|
-
logger.info(
|
|
2470
|
-
? '[ClaudeSandbox]
|
|
2471
|
-
:
|
|
2572
|
+
logger.info(callPermissionMode === 'fullaccess'
|
|
2573
|
+
? '[ClaudeSandbox] disabled for trusted per-call fullaccess execution'
|
|
2574
|
+
: process.platform === 'linux'
|
|
2575
|
+
? '[ClaudeSandbox] task sandbox disabled for authenticated owner bypass; outer H-class guard remains active'
|
|
2576
|
+
: '[ClaudeSandbox] disabled for authenticated owner bypass task');
|
|
2472
2577
|
}
|
|
2473
2578
|
const handleClaudeStderr = (msg) => {
|
|
2474
2579
|
const trimmed = msg.trim();
|
|
@@ -2494,16 +2599,17 @@ export class AgentRunner {
|
|
|
2494
2599
|
model: sdkModel,
|
|
2495
2600
|
...capabilityOptions,
|
|
2496
2601
|
strictMcpConfig: false,
|
|
2497
|
-
managedSettings,
|
|
2602
|
+
...(managedSettings ? { managedSettings } : {}),
|
|
2498
2603
|
...(callEffort ? { effort: callEffort } : {}),
|
|
2499
2604
|
...(this.claudeExecutablePath ? { pathToClaudeCodeExecutable: this.claudeExecutablePath } : {}),
|
|
2500
|
-
...(executionSandbox.state === 'off' && process.platform === 'linux' ? {
|
|
2605
|
+
...(executionSandbox.state === 'off' && callPermissionMode !== 'fullaccess' && process.platform === 'linux' ? {
|
|
2501
2606
|
spawnClaudeCodeProcess: (options) => spawnClaudeWithHClassGuard(options, resolvePaths().root, handleClaudeStderr),
|
|
2502
2607
|
} : {}),
|
|
2503
2608
|
autoCompactWindow: autoCompactWindowForModel(sdkModel),
|
|
2504
2609
|
advisorModel: 'haiku',
|
|
2505
2610
|
canUseTool: canUseToolCallback,
|
|
2506
2611
|
permissionMode: sdkPermissionMode,
|
|
2612
|
+
...(callPermissionMode === 'fullaccess' ? { allowDangerouslySkipPermissions: true } : {}),
|
|
2507
2613
|
sandbox: sandboxOptions,
|
|
2508
2614
|
persistSession: true,
|
|
2509
2615
|
includePartialMessages: true,
|
|
@@ -2625,9 +2731,14 @@ export class AgentRunner {
|
|
|
2625
2731
|
const currentSession = await sessionManager.getSessionById?.(sessionId);
|
|
2626
2732
|
if (currentSession?.metadata?.resumeAt) {
|
|
2627
2733
|
resumeAt = currentSession.metadata.resumeAt;
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2734
|
+
if (typeof sessionManager.patchSessionMetadata === 'function') {
|
|
2735
|
+
await sessionManager.patchSessionMetadata(sessionId, { resumeAt: undefined });
|
|
2736
|
+
}
|
|
2737
|
+
else {
|
|
2738
|
+
const newMeta = { ...currentSession.metadata };
|
|
2739
|
+
delete newMeta.resumeAt;
|
|
2740
|
+
await sessionManager.updateSession(sessionId, { metadata: newMeta });
|
|
2741
|
+
}
|
|
2631
2742
|
logger.info(`[AgentRunner] Consuming resumeAt: ${resumeAt}`);
|
|
2632
2743
|
}
|
|
2633
2744
|
}
|
|
@@ -2639,10 +2750,14 @@ export class AgentRunner {
|
|
|
2639
2750
|
const msgStream = new MessageStream();
|
|
2640
2751
|
if (images && images.length > 0) {
|
|
2641
2752
|
logger.info('[AgentRunner] Creating query with images:', images.length, 'first image size:', images[0]?.data?.length ?? 0);
|
|
2642
|
-
logger.debug('[AgentRunner] Skipping resume for image message to avoid history conflict');
|
|
2643
2753
|
msgStream.push(prompt, images, inputId);
|
|
2644
2754
|
try {
|
|
2645
|
-
|
|
2755
|
+
// Topic bindings are authoritative for every input shape. Starting
|
|
2756
|
+
// an unresumed Claude query for a bound topic would discover a second
|
|
2757
|
+
// backend and then fail activation with a binding conflict.
|
|
2758
|
+
sdkStream = topicBinding
|
|
2759
|
+
? createQuery(msgStream, agentSessionId, resumeAt)
|
|
2760
|
+
: createQuery(msgStream);
|
|
2646
2761
|
}
|
|
2647
2762
|
catch (error) {
|
|
2648
2763
|
this.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
|
|
@@ -2676,14 +2791,16 @@ export class AgentRunner {
|
|
|
2676
2791
|
}
|
|
2677
2792
|
this.activeMessageStreams.set(sessionId, msgStream);
|
|
2678
2793
|
this.activeQueries.set(sessionId, sdkStream);
|
|
2679
|
-
|
|
2794
|
+
let resolveDone;
|
|
2795
|
+
const donePromise = new Promise(resolve => { resolveDone = resolve; });
|
|
2796
|
+
this.streamDoneResolvers.set(sessionId, resolveDone);
|
|
2680
2797
|
this.streamDone.set(sessionId, donePromise);
|
|
2681
2798
|
// 保存 interrupt 能力(不写 activeStreams,由 registerStream 管理活跃状态)
|
|
2682
2799
|
if ('interrupt' in sdkStream && typeof sdkStream.interrupt === 'function') {
|
|
2683
2800
|
this.interruptFns.set(sessionId, () => sdkStream.interrupt());
|
|
2684
2801
|
}
|
|
2685
2802
|
// 返回标准 AgentEvent 流(重试由 MessageProcessor 层负责)
|
|
2686
|
-
const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode);
|
|
2803
|
+
const transformed = this.transformStream(sdkStream, sessionId, inputId, callModel, callEffort, sdkModel, !!agentSessionId, sandboxSettingsFile, callPermissionMode, modelOverride?.turn);
|
|
2687
2804
|
const self = this;
|
|
2688
2805
|
return (async function* () {
|
|
2689
2806
|
try {
|
|
@@ -2691,10 +2808,15 @@ export class AgentRunner {
|
|
|
2691
2808
|
}
|
|
2692
2809
|
finally {
|
|
2693
2810
|
self.cleanupSandboxSettingsFile(sessionId, sandboxSettingsFile);
|
|
2694
|
-
|
|
2695
|
-
self.streamDoneResolvers.
|
|
2696
|
-
|
|
2697
|
-
self.
|
|
2811
|
+
resolveDone();
|
|
2812
|
+
if (self.streamDoneResolvers.get(sessionId) === resolveDone)
|
|
2813
|
+
self.streamDoneResolvers.delete(sessionId);
|
|
2814
|
+
if (self.streamDone.get(sessionId) === donePromise)
|
|
2815
|
+
self.streamDone.delete(sessionId);
|
|
2816
|
+
if (self.activeQueries.get(sessionId) === sdkStream)
|
|
2817
|
+
self.activeQueries.delete(sessionId);
|
|
2818
|
+
if (self.activeMessageStreams.get(sessionId) === msgStream)
|
|
2819
|
+
self.activeMessageStreams.delete(sessionId);
|
|
2698
2820
|
}
|
|
2699
2821
|
})();
|
|
2700
2822
|
}
|
|
@@ -2762,19 +2884,64 @@ export class AgentRunner {
|
|
|
2762
2884
|
injectUserMessage(sessionId, text) {
|
|
2763
2885
|
this.activeMessageStreams.get(sessionId)?.push(text);
|
|
2764
2886
|
}
|
|
2887
|
+
async acceptDiscoveredSessionId(sessionId, agentSessionId, turn) {
|
|
2888
|
+
const result = this.onSessionIdUpdate
|
|
2889
|
+
? (turn === undefined
|
|
2890
|
+
? await this.onSessionIdUpdate(sessionId, agentSessionId)
|
|
2891
|
+
: await this.onSessionIdUpdate(sessionId, agentSessionId, { turn }))
|
|
2892
|
+
: 'legacy_updated';
|
|
2893
|
+
const accepted = result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated';
|
|
2894
|
+
if (accepted) {
|
|
2895
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
2896
|
+
}
|
|
2897
|
+
return accepted;
|
|
2898
|
+
}
|
|
2765
2899
|
updateSessionId(sessionId, agentSessionId) {
|
|
2766
2900
|
logger.info(`[AgentRunner] updateSessionId called: sessionId=${sessionId}, agentSessionId=${agentSessionId}`);
|
|
2767
|
-
this.activeSessions.
|
|
2768
|
-
|
|
2769
|
-
this.
|
|
2901
|
+
const previousAgentSessionId = this.activeSessions.get(sessionId);
|
|
2902
|
+
const canApply = () => {
|
|
2903
|
+
const current = this.activeSessions.get(sessionId);
|
|
2904
|
+
return current === previousAgentSessionId || (previousAgentSessionId === undefined && current === undefined);
|
|
2905
|
+
};
|
|
2906
|
+
if (!this.onSessionIdUpdate) {
|
|
2907
|
+
if (!canApply())
|
|
2908
|
+
return;
|
|
2909
|
+
if (agentSessionId)
|
|
2910
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
2911
|
+
else if (this.activeSessions.get(sessionId) === previousAgentSessionId)
|
|
2912
|
+
this.activeSessions.delete(sessionId);
|
|
2913
|
+
return;
|
|
2770
2914
|
}
|
|
2915
|
+
void Promise.resolve(this.onSessionIdUpdate(sessionId, agentSessionId)).then(result => {
|
|
2916
|
+
if (!canApply())
|
|
2917
|
+
return;
|
|
2918
|
+
if (result === undefined || result === 'activated' || result === 'already_active' || result === 'legacy_updated') {
|
|
2919
|
+
if (agentSessionId)
|
|
2920
|
+
this.activeSessions.set(sessionId, agentSessionId);
|
|
2921
|
+
else if (this.activeSessions.get(sessionId) === previousAgentSessionId)
|
|
2922
|
+
this.activeSessions.delete(sessionId);
|
|
2923
|
+
}
|
|
2924
|
+
else if (!agentSessionId) {
|
|
2925
|
+
if (this.activeSessions.get(sessionId) === previousAgentSessionId)
|
|
2926
|
+
this.activeSessions.delete(sessionId);
|
|
2927
|
+
}
|
|
2928
|
+
}).catch(error => {
|
|
2929
|
+
logger.warn(`[AgentRunner] session binding callback failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2930
|
+
if (!agentSessionId && canApply() && this.activeSessions.get(sessionId) === previousAgentSessionId) {
|
|
2931
|
+
this.activeSessions.delete(sessionId);
|
|
2932
|
+
}
|
|
2933
|
+
});
|
|
2771
2934
|
}
|
|
2772
|
-
runSessionCommand(prompt, agentSessionId, projectPath) {
|
|
2935
|
+
runSessionCommand(prompt, agentSessionId, projectPath, modelOverride) {
|
|
2936
|
+
const callModel = modelOverride?.model || this.model;
|
|
2937
|
+
const callEffort = (modelOverride?.effortMode === 'model_default'
|
|
2938
|
+
? undefined
|
|
2939
|
+
: modelOverride?.effort ?? this.effort);
|
|
2773
2940
|
return query({
|
|
2774
2941
|
prompt,
|
|
2775
2942
|
options: {
|
|
2776
2943
|
cwd: projectPath,
|
|
2777
|
-
model: resolveSdkModel(
|
|
2944
|
+
model: resolveSdkModel(callModel, this.baseUrl),
|
|
2778
2945
|
resume: agentSessionId,
|
|
2779
2946
|
maxTurns: 1,
|
|
2780
2947
|
tools: [],
|
|
@@ -2783,6 +2950,7 @@ export class AgentRunner {
|
|
|
2783
2950
|
strictMcpConfig: true,
|
|
2784
2951
|
settingSources: [],
|
|
2785
2952
|
managedSettings: buildClaudeManagedLockdownSettings(),
|
|
2953
|
+
...(callEffort ? { effort: callEffort } : {}),
|
|
2786
2954
|
permissionMode: this.toSdkPermissionMode(),
|
|
2787
2955
|
env: this.getAgentEnv()
|
|
2788
2956
|
}
|
|
@@ -2791,31 +2959,74 @@ export class AgentRunner {
|
|
|
2791
2959
|
/**
|
|
2792
2960
|
* 主动压缩会话上下文
|
|
2793
2961
|
*/
|
|
2794
|
-
async compactSession(sessionId, agentSessionId, projectPath) {
|
|
2962
|
+
async compactSession(sessionId, agentSessionId, projectPath, modelOverride) {
|
|
2963
|
+
const startedAt = Date.now();
|
|
2964
|
+
let eventCount = 0;
|
|
2965
|
+
let lastEvent;
|
|
2795
2966
|
try {
|
|
2796
|
-
logger.info(`[AgentRunner] Compacting session:
|
|
2797
|
-
const stream = this.runSessionCommand('/compact', agentSessionId, projectPath);
|
|
2967
|
+
logger.info(`[AgentRunner] Compacting session: session=${sessionId} agent=${agentSessionId} project=${projectPath}`);
|
|
2968
|
+
const stream = this.runSessionCommand('/compact', agentSessionId, projectPath, modelOverride);
|
|
2798
2969
|
this.activeStreams.set(sessionId, stream);
|
|
2799
2970
|
try {
|
|
2800
|
-
let
|
|
2971
|
+
let boundary;
|
|
2972
|
+
let compactStatus;
|
|
2801
2973
|
for await (const event of stream) {
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2974
|
+
eventCount += 1;
|
|
2975
|
+
lastEvent = { type: event?.type, subtype: event?.subtype };
|
|
2976
|
+
if (event.type === 'system' && event.subtype === 'status' && event.compact_result) {
|
|
2977
|
+
compactStatus = {
|
|
2978
|
+
result: event.compact_result,
|
|
2979
|
+
error: event.compact_error,
|
|
2980
|
+
};
|
|
2805
2981
|
}
|
|
2982
|
+
if (event.type === 'system' && event.subtype === 'compact_boundary')
|
|
2983
|
+
boundary = event;
|
|
2984
|
+
}
|
|
2985
|
+
const durationMs = Date.now() - startedAt;
|
|
2986
|
+
if (compactStatus?.result === 'failed') {
|
|
2987
|
+
const result = {
|
|
2988
|
+
ok: false,
|
|
2989
|
+
code: 'sdk_error',
|
|
2990
|
+
message: compactStatus.error || 'Claude SDK reported compact failure',
|
|
2991
|
+
durationMs,
|
|
2992
|
+
eventCount,
|
|
2993
|
+
lastEvent,
|
|
2994
|
+
};
|
|
2995
|
+
logger.warn(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} code=${result.code} events=${eventCount} durationMs=${durationMs} message=${result.message}`);
|
|
2996
|
+
return result;
|
|
2806
2997
|
}
|
|
2807
|
-
if (!
|
|
2808
|
-
|
|
2998
|
+
if (!boundary) {
|
|
2999
|
+
const result = {
|
|
3000
|
+
ok: false,
|
|
3001
|
+
code: 'missing_boundary',
|
|
3002
|
+
message: 'Compact stream ended without compact_boundary event',
|
|
3003
|
+
durationMs,
|
|
3004
|
+
eventCount,
|
|
3005
|
+
lastEvent,
|
|
3006
|
+
};
|
|
3007
|
+
logger.warn(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} code=${result.code} events=${eventCount} durationMs=${durationMs}`);
|
|
3008
|
+
return result;
|
|
2809
3009
|
}
|
|
2810
|
-
|
|
3010
|
+
const result = {
|
|
3011
|
+
ok: true,
|
|
3012
|
+
durationMs,
|
|
3013
|
+
eventCount,
|
|
3014
|
+
preTokens: boundary.compact_metadata?.pre_tokens,
|
|
3015
|
+
postTokens: boundary.compact_metadata?.post_tokens,
|
|
3016
|
+
};
|
|
3017
|
+
logger.info(`[AgentRunner] Compact completed: session=${sessionId} agent=${agentSessionId} events=${eventCount} durationMs=${durationMs} pre_tokens=${result.preTokens ?? 'unknown'} post_tokens=${result.postTokens ?? 'unknown'}`);
|
|
3018
|
+
return result;
|
|
2811
3019
|
}
|
|
2812
3020
|
finally {
|
|
2813
3021
|
this.activeStreams.delete(sessionId);
|
|
2814
3022
|
}
|
|
2815
3023
|
}
|
|
2816
3024
|
catch (error) {
|
|
2817
|
-
|
|
2818
|
-
|
|
3025
|
+
const durationMs = Date.now() - startedAt;
|
|
3026
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3027
|
+
const result = { ok: false, code: 'sdk_error', message, durationMs, eventCount, lastEvent };
|
|
3028
|
+
logger.error(`[AgentRunner] Compact failed: session=${sessionId} agent=${agentSessionId} project=${projectPath} code=${result.code} events=${eventCount} durationMs=${durationMs} error=${message}`);
|
|
3029
|
+
return result;
|
|
2819
3030
|
}
|
|
2820
3031
|
}
|
|
2821
3032
|
/**
|
|
@@ -2836,7 +3047,7 @@ export class AgentRunner {
|
|
|
2836
3047
|
}
|
|
2837
3048
|
if (cleared) {
|
|
2838
3049
|
this.activeSessions.delete(sessionId);
|
|
2839
|
-
this.onSessionIdUpdate?.(sessionId, '');
|
|
3050
|
+
await this.onSessionIdUpdate?.(sessionId, '');
|
|
2840
3051
|
}
|
|
2841
3052
|
else {
|
|
2842
3053
|
logger.warn('[AgentRunner] Clear stream ended without session reset signal');
|
|
@@ -2853,16 +3064,76 @@ export class AgentRunner {
|
|
|
2853
3064
|
}
|
|
2854
3065
|
}
|
|
2855
3066
|
async closeSession(sessionId) {
|
|
3067
|
+
const capturedAgentSessionId = this.activeSessions.get(sessionId);
|
|
3068
|
+
const capturedPermissionContext = this.permissionContexts.get(sessionId);
|
|
2856
3069
|
this.permissionContexts.get(sessionId)?.pauseController?.cancel();
|
|
2857
3070
|
const query = this.activeQueries.get(sessionId);
|
|
3071
|
+
const messageStream = this.activeMessageStreams.get(sessionId);
|
|
3072
|
+
const registeredStream = this.activeStreams.get(sessionId);
|
|
3073
|
+
const interrupt = this.interruptFns.get(sessionId);
|
|
3074
|
+
const done = this.streamDone.get(sessionId);
|
|
3075
|
+
const resolveDone = this.streamDoneResolvers.get(sessionId);
|
|
3076
|
+
let interruptError;
|
|
3077
|
+
if (interrupt) {
|
|
3078
|
+
try {
|
|
3079
|
+
await interrupt();
|
|
3080
|
+
}
|
|
3081
|
+
catch (error) {
|
|
3082
|
+
interruptError = error;
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
messageStream?.end();
|
|
2858
3086
|
if (query && typeof query.close === 'function')
|
|
2859
|
-
query.close();
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
3087
|
+
await query.close();
|
|
3088
|
+
if (done && registeredStream) {
|
|
3089
|
+
let timer;
|
|
3090
|
+
try {
|
|
3091
|
+
await Promise.race([
|
|
3092
|
+
done,
|
|
3093
|
+
new Promise((_, reject) => {
|
|
3094
|
+
timer = setTimeout(() => reject(new Error(`Claude session did not become quiescent: ${sessionId}`)), 30_000);
|
|
3095
|
+
timer.unref?.();
|
|
3096
|
+
}),
|
|
3097
|
+
]);
|
|
3098
|
+
}
|
|
3099
|
+
finally {
|
|
3100
|
+
if (timer)
|
|
3101
|
+
clearTimeout(timer);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
else if (done) {
|
|
3105
|
+
// runQuery resolved but ResponseEngine never registered/consumed the
|
|
3106
|
+
// wrapper. No consumer can reach its finally block, so close the exact
|
|
3107
|
+
// captured lifecycle explicitly.
|
|
3108
|
+
resolveDone?.();
|
|
3109
|
+
}
|
|
3110
|
+
const lifecycleReplaced = (query && this.activeQueries.get(sessionId) !== undefined && this.activeQueries.get(sessionId) !== query)
|
|
3111
|
+
|| (messageStream && this.activeMessageStreams.get(sessionId) !== undefined && this.activeMessageStreams.get(sessionId) !== messageStream)
|
|
3112
|
+
|| (registeredStream && this.activeStreams.get(sessionId) !== undefined && this.activeStreams.get(sessionId) !== registeredStream);
|
|
3113
|
+
if (lifecycleReplaced) {
|
|
3114
|
+
const error = new Error(`Claude session lifecycle changed during close: ${sessionId}`);
|
|
3115
|
+
error.code = 'BACKEND_RUNTIME_REPLACED';
|
|
3116
|
+
throw error;
|
|
3117
|
+
}
|
|
3118
|
+
if (this.activeQueries.get(sessionId) === query)
|
|
3119
|
+
this.activeQueries.delete(sessionId);
|
|
3120
|
+
if (this.activeMessageStreams.get(sessionId) === messageStream)
|
|
3121
|
+
this.activeMessageStreams.delete(sessionId);
|
|
3122
|
+
if (this.activeStreams.get(sessionId) === registeredStream)
|
|
3123
|
+
this.activeStreams.delete(sessionId);
|
|
3124
|
+
if (this.interruptFns.get(sessionId) === interrupt)
|
|
3125
|
+
this.interruptFns.delete(sessionId);
|
|
3126
|
+
if (this.streamDone.get(sessionId) === done)
|
|
3127
|
+
this.streamDone.delete(sessionId);
|
|
3128
|
+
if (this.streamDoneResolvers.get(sessionId) === resolveDone)
|
|
3129
|
+
this.streamDoneResolvers.delete(sessionId);
|
|
3130
|
+
if (this.activeSessions.get(sessionId) === capturedAgentSessionId)
|
|
3131
|
+
this.activeSessions.delete(sessionId);
|
|
3132
|
+
if (this.permissionContexts.get(sessionId) === capturedPermissionContext)
|
|
3133
|
+
this.permissionContexts.delete(sessionId);
|
|
2865
3134
|
this.cleanupSandboxSettingsFiles(sessionId);
|
|
3135
|
+
if (interruptError)
|
|
3136
|
+
throw interruptError;
|
|
2866
3137
|
}
|
|
2867
3138
|
async dispose() {
|
|
2868
3139
|
const interrupts = [...this.interruptFns.values()].map(async (interrupt) => {
|
|
@@ -2978,9 +3249,9 @@ export class ClaudeAgentPlugin {
|
|
|
2978
3249
|
const syntheticConfig = { agents: { claude: override } };
|
|
2979
3250
|
const anthropic = resolveAnthropicConfig(syntheticConfig, override);
|
|
2980
3251
|
const merged = {
|
|
2981
|
-
agents: { claude: { ...(override || {})
|
|
3252
|
+
agents: { claude: { ...(override || {}) } },
|
|
2982
3253
|
};
|
|
2983
|
-
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged);
|
|
3254
|
+
const agentRunner = new AgentRunner(anthropic.apiKey, anthropic.model, callbacks.onSessionIdUpdate, anthropic.baseUrl, merged, { agentAid: agent.aid, agentConfig: agent.config });
|
|
2984
3255
|
if (anthropic.effort) {
|
|
2985
3256
|
agentRunner.setEffort(anthropic.effort);
|
|
2986
3257
|
}
|