evolcore 0.0.13 → 0.0.15
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 +27 -0
- package/bin/codex-managed-hook.mjs +4 -1
- package/bin/install-codex-managed-hooks.mjs +201 -0
- package/dist/agents/claude-runner.js +53 -5
- package/dist/agents/codex-app-server-client.js +123 -2
- package/dist/agents/codex-runner.js +152 -30
- package/dist/agents/ecagent-runner.js +17 -1
- package/dist/agents/gemini-runner.js +9 -4
- package/dist/aun/msg/managed-operation.js +63 -3
- package/dist/channels/aun.js +144 -15
- package/dist/channels/daemon.js +2 -0
- package/dist/channels/feishu.js +6 -1
- package/dist/cli/aun-commands.js +1 -1
- package/dist/cli/fs-command.js +46 -9
- package/dist/cli/task-context.js +176 -0
- package/dist/config/builtin-roles.js +2 -0
- package/dist/config/config-manager.js +6 -2
- package/dist/config/contact-book-store.js +7 -2
- package/dist/core/auth/auth-gateway.js +1 -0
- package/dist/core/auth/authorization-audit.js +32 -0
- package/dist/core/auth/operation-catalog.js +3 -3
- package/dist/core/bootstrap-service.js +7 -1
- package/dist/core/command/command-handler.js +3 -0
- package/dist/core/command/slash-handler.js +1 -1
- package/dist/core/event-catalog.js +2 -0
- package/dist/core/message/im-renderer.js +15 -1
- package/dist/core/message/message-bridge.js +5 -2
- package/dist/core/message/response-engine.js +138 -10
- package/dist/core/permission/approval-gateway.js +99 -16
- package/dist/core/permission/ec-command-parser.js +556 -4
- package/dist/core/permission/tool-policy.js +17 -29
- package/dist/core/runtime-lock.js +101 -0
- package/dist/index.js +30 -3
- package/dist/response-system/engines/v1/proactive-flow.js +92 -8
- package/dist/response-system/modes/single-session/index.js +3 -0
- package/dist/trigger/history.js +42 -7
- package/dist/utils/error-utils.js +7 -0
- package/dist/utils/logger.js +37 -4
- package/kits/templates/roles/admin.json +2 -0
- package/kits/templates/roles/member.json +1 -0
- package/package.json +1 -1
|
@@ -14,9 +14,40 @@ export function auditCommandAuthorization(event) {
|
|
|
14
14
|
const auditRecord = buildAuditRecord(event);
|
|
15
15
|
logAuditEvent(auditRecord);
|
|
16
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Record a tool-level preflight denial (e.g. protected-path block) in the
|
|
19
|
+
* structured command-audit log. This covers interactive sessions whose
|
|
20
|
+
* denials otherwise only reach `logger.warn` and `TriggerExecutionAnomaly`.
|
|
21
|
+
*/
|
|
22
|
+
export function auditToolPreflightDenial(input) {
|
|
23
|
+
auditCommandAuthorization({
|
|
24
|
+
ts: Date.now(),
|
|
25
|
+
source: 'agent-tool',
|
|
26
|
+
operation: 'tool.preflight.deny',
|
|
27
|
+
scope: 'filesystem',
|
|
28
|
+
dangerous: false,
|
|
29
|
+
decision: 'deny',
|
|
30
|
+
decisionSource: 'policy',
|
|
31
|
+
toolName: input.toolName,
|
|
32
|
+
policyCode: input.policyCode,
|
|
33
|
+
reason: input.reason,
|
|
34
|
+
argsSummary: input.summary ? { summary: input.summary } : undefined,
|
|
35
|
+
sessionId: input.sessionId,
|
|
36
|
+
agentAid: input.agentAid,
|
|
37
|
+
permissionMode: input.permissionMode,
|
|
38
|
+
channel: input.channel,
|
|
39
|
+
actorId: input.actorId,
|
|
40
|
+
role: input.role ?? 'unknown',
|
|
41
|
+
selfAid: input.selfAid,
|
|
42
|
+
peerKey: input.peerKey,
|
|
43
|
+
requestId: input.requestId,
|
|
44
|
+
taskId: input.taskId,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
17
47
|
function buildAuditRecord(event) {
|
|
18
48
|
return {
|
|
19
49
|
ts: event.ts,
|
|
50
|
+
correlationId: event.correlationId ?? event.requestId,
|
|
20
51
|
requestId: event.requestId,
|
|
21
52
|
sessionId: event.sessionId,
|
|
22
53
|
agentAid: redactIdentifier(event.agentAid ?? event.selfAid),
|
|
@@ -79,6 +110,7 @@ function logAuditEvent(record) {
|
|
|
79
110
|
record.taskId ? `task=${record.taskId}` : null,
|
|
80
111
|
record.messageId ? `message=${record.messageId}` : null,
|
|
81
112
|
record.requestId ? `request=${record.requestId}` : null,
|
|
113
|
+
record.correlationId ? `correlation=${record.correlationId}` : null,
|
|
82
114
|
record.sessionId ? `session=${record.sessionId}` : null,
|
|
83
115
|
record.agentAid ? `agent=${record.agentAid}` : null,
|
|
84
116
|
record.permissionMode ? `permissionMode=${record.permissionMode}` : null,
|
|
@@ -65,7 +65,7 @@ const MSG_READ_OPERATIONS = [
|
|
|
65
65
|
description,
|
|
66
66
|
sources: ['agent-tool'],
|
|
67
67
|
}));
|
|
68
|
-
const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df'].map(kind => ({
|
|
68
|
+
const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df', 'getfacl'].map(kind => ({
|
|
69
69
|
id: `ec.fs.${kind}`,
|
|
70
70
|
category: 'read',
|
|
71
71
|
dangerous: false,
|
|
@@ -73,9 +73,9 @@ const GROUP_FS_READ_OPERATIONS = ['ls', 'stat', 'lstat', 'cat', 'find', 'df'].ma
|
|
|
73
73
|
description: `Read current group filesystem (${kind})`,
|
|
74
74
|
sources: ['agent-tool'],
|
|
75
75
|
}));
|
|
76
|
-
const GROUP_FS_WRITE_OPERATIONS = ['mkdir', 'upload', 'copy', 'move', 'remove'].map(kind => ({
|
|
76
|
+
const GROUP_FS_WRITE_OPERATIONS = [['mkdir', 'write-own'], ['upload', 'write-own'], ['copy', 'write-own'], ['move', 'write-own'], ['remove', 'write-own'], ['setfacl', 'write-agent']].map(([kind, category]) => ({
|
|
77
77
|
id: `ec.fs.${kind}`,
|
|
78
|
-
category
|
|
78
|
+
category,
|
|
79
79
|
dangerous: false,
|
|
80
80
|
defaultScopes: ['relation'],
|
|
81
81
|
description: `Write current group filesystem (${kind})`,
|
|
@@ -140,7 +140,13 @@ export class BootstrapService {
|
|
|
140
140
|
this.inFlight.delete(key);
|
|
141
141
|
return false;
|
|
142
142
|
}
|
|
143
|
-
|
|
143
|
+
// The bootstrap prompt is deliberately a private message to the configured
|
|
144
|
+
// owner. An inbound owner message may have arrived from a group, in which
|
|
145
|
+
// case ctx.channelId is the group AID and must not be paired with the
|
|
146
|
+
// private delivery route below (that would call message.send(to=group)).
|
|
147
|
+
const channelId = channelType === 'aun'
|
|
148
|
+
? recipientId
|
|
149
|
+
: ctx.channelId || this.defaultChannelIdForConnection(channelType, recipientId);
|
|
144
150
|
if (!channelId) {
|
|
145
151
|
this.inFlight.delete(key);
|
|
146
152
|
return false;
|
|
@@ -1974,6 +1974,9 @@ export class CommandHandler {
|
|
|
1974
1974
|
...(parsed.command.recursive !== undefined ? { recursive: parsed.command.recursive } : {}),
|
|
1975
1975
|
...(parsed.command.parents !== undefined ? { parents: parsed.command.parents } : {}),
|
|
1976
1976
|
...(parsed.command.overwrite !== undefined ? { overwrite: parsed.command.overwrite } : {}),
|
|
1977
|
+
...(parsed.command.aclGrantee ? { aclGrantee: parsed.command.aclGrantee } : {}),
|
|
1978
|
+
...(parsed.command.aclPerms ? { aclPerms: parsed.command.aclPerms } : {}),
|
|
1979
|
+
...(parsed.command.aclRemove !== undefined ? { aclRemove: parsed.command.aclRemove } : {}),
|
|
1977
1980
|
};
|
|
1978
1981
|
const decision = authorizeOperation({
|
|
1979
1982
|
source: 'agent-tool',
|
|
@@ -705,7 +705,7 @@ export async function handleSlashCommand(content, channel, channelId, sendMessag
|
|
|
705
705
|
const args = normalizedContent.slice(5).trim();
|
|
706
706
|
const permissionDecisionLabels = {
|
|
707
707
|
allow: '✓ 已授权(本次),继续执行……',
|
|
708
|
-
always: '✓
|
|
708
|
+
always: '✓ 已授权(当前会话临时授权 30 分钟),继续执行……',
|
|
709
709
|
deny: '✓ 已拒绝',
|
|
710
710
|
};
|
|
711
711
|
// Explicit request IDs are globally unique and already bound to an exact
|
|
@@ -314,6 +314,7 @@ const CATALOG = [
|
|
|
314
314
|
{ path: 'toolName', type: 'string' },
|
|
315
315
|
{ path: 'input', type: 'object' },
|
|
316
316
|
{ path: 'callId', type: 'string', optional: true },
|
|
317
|
+
{ path: 'correlationId', type: 'string', optional: true },
|
|
317
318
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
318
319
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
319
320
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
@@ -330,6 +331,7 @@ const CATALOG = [
|
|
|
330
331
|
{ path: 'isError', type: 'boolean', optional: true },
|
|
331
332
|
{ path: 'agentName', type: 'string', optional: true },
|
|
332
333
|
{ path: 'callId', type: 'string', optional: true },
|
|
334
|
+
{ path: 'correlationId', type: 'string', optional: true },
|
|
333
335
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
334
336
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
335
337
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
@@ -59,7 +59,21 @@ export class IMRenderer {
|
|
|
59
59
|
/** 推入 AgentEvent,按 chatmode 投影 */
|
|
60
60
|
emit(event) {
|
|
61
61
|
try {
|
|
62
|
-
|
|
62
|
+
const correlatedEvent = event.type === 'tool_use' || event.type === 'tool_result'
|
|
63
|
+
? { ...event, correlationId: event.correlationId ?? event.callId }
|
|
64
|
+
: event;
|
|
65
|
+
logger.event({
|
|
66
|
+
source: 'runner',
|
|
67
|
+
taskId: this.opts.envelope.taskId,
|
|
68
|
+
sessionId: this.opts.envelope.sessionId,
|
|
69
|
+
channelId: this.opts.envelope.channelId,
|
|
70
|
+
agentAid: this.opts.agentAid,
|
|
71
|
+
agentName: this.opts.envelope.agentName,
|
|
72
|
+
...(correlatedEvent.type === 'tool_use' || correlatedEvent.type === 'tool_result'
|
|
73
|
+
? { correlationId: correlatedEvent.correlationId, toolName: correlatedEvent.name }
|
|
74
|
+
: {}),
|
|
75
|
+
event: correlatedEvent,
|
|
76
|
+
});
|
|
63
77
|
}
|
|
64
78
|
catch {
|
|
65
79
|
// logger.event 失败不影响业务
|
|
@@ -872,7 +872,7 @@ export class MessageBridge {
|
|
|
872
872
|
return;
|
|
873
873
|
}
|
|
874
874
|
// 普通消息保留原始日志;控制 payload 使用脱敏元数据日志。
|
|
875
|
-
logger.channelIn({ channel: channelName, channelId: msg.channelId, peerId: msg.peerId, peerName: msg.peerName, chatType: msg.chatType, msgId: msg.messageId, threadId: msg.threadId, content, images: msg.images?.length ?? 0, mentions: msg.mentions, replyContext: msg.replyContext });
|
|
875
|
+
logger.channelIn({ channel: channelName, channelId: msg.channelId, selfAid: msg.selfAID, peerId: msg.peerId, peerName: msg.peerName, chatType: msg.chatType, msgId: msg.messageId, correlationId: msg.messageId, threadId: msg.threadId, content, images: msg.images?.length ?? 0, mentions: msg.mentions, replyContext: msg.replyContext });
|
|
876
876
|
const accessDecision = authorizeAccess(authSubject);
|
|
877
877
|
if (!accessDecision.allow) {
|
|
878
878
|
logger.warn(`[MessageBridge] Access denied before command routing: channel=${channelName} channelId=${msg.channelId} actor=${actorId ?? '<none>'} role=${identity.role} reason=${accessDecision.reason}`);
|
|
@@ -954,7 +954,8 @@ export class MessageBridge {
|
|
|
954
954
|
}
|
|
955
955
|
}
|
|
956
956
|
if (msg.source !== 'handoff' && await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
|
|
957
|
-
|
|
957
|
+
const taskId = `cmd-${msg.messageId || Date.now()}`;
|
|
958
|
+
logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
|
|
958
959
|
return sendReply(msg.channelId, text, msg.replyContext);
|
|
959
960
|
}, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
|
|
960
961
|
return;
|
|
@@ -1468,9 +1469,11 @@ export class MessageBridge {
|
|
|
1468
1469
|
logger.channelIn({
|
|
1469
1470
|
channel,
|
|
1470
1471
|
channelId: this.shortHash(msg.channelId),
|
|
1472
|
+
selfAid: msg.selfAID,
|
|
1471
1473
|
peerId: this.shortHash(msg.peerId),
|
|
1472
1474
|
chatType: msg.chatType,
|
|
1473
1475
|
msgId: msg.messageId,
|
|
1476
|
+
correlationId: msg.messageId,
|
|
1474
1477
|
control: {
|
|
1475
1478
|
id: parsed.id,
|
|
1476
1479
|
type: parsed.type,
|
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { BaseagentRunnerUnavailableError, hasCompact, autoCompactWindowForContextWindow, autoCompactWindowForModel, isClaudeContextUsageModel, numericToken } from '../../agents/runner-types.js';
|
|
6
|
-
import { buildTaskRuntimeEnv } from '../../cli/task-context.js';
|
|
6
|
+
import { buildTaskRuntimeEnv, ensureRuntimeLockDir, ensureSessionRuntimeDir } from '../../cli/task-context.js';
|
|
7
7
|
import { IMRenderer } from './im-renderer.js';
|
|
8
8
|
import { createSendFileMarkerPattern } from './file-markers.js';
|
|
9
9
|
import { createTextInferenceProvider } from '../inference/text-inference.js';
|
|
@@ -953,7 +953,7 @@ export class ResponseEngine {
|
|
|
953
953
|
}
|
|
954
954
|
agent.registerStream(streamKey, retryStream);
|
|
955
955
|
return await this.processEventStream(retryStream, session, agent, renderer, resetTimer, shouldSuppress, proactive, undefined, // 重试分支不调插件钩子
|
|
956
|
-
undefined, turnLease, opts.permissionMode);
|
|
956
|
+
undefined, turnLease, opts.permissionMode, opts.proactiveSelfAid);
|
|
957
957
|
}
|
|
958
958
|
// Dropping the whole session hides the root cause and can replay side
|
|
959
959
|
// effects in unattended trigger tasks. Surface an actionable failure
|
|
@@ -1992,6 +1992,7 @@ export class ResponseEngine {
|
|
|
1992
1992
|
const renderer = new IMRenderer({
|
|
1993
1993
|
adapter,
|
|
1994
1994
|
envelope,
|
|
1995
|
+
agentAid: session.selfAID,
|
|
1995
1996
|
flushDelay: (options?.flushDelay ?? this.agentRegistry?.resolveByChannel(channelKey)?.config?.flush_delay ?? 3) * 1000,
|
|
1996
1997
|
suppressActivityItems: isProactive ? false : middleOutputMode !== 'all',
|
|
1997
1998
|
suppressIntermediateText: isProactive ? false : middleOutputMode === 'none',
|
|
@@ -2170,6 +2171,7 @@ export class ResponseEngine {
|
|
|
2170
2171
|
? (anomaly) => {
|
|
2171
2172
|
recordTriggerExecutionAnomaly(triggerRunId, {
|
|
2172
2173
|
...anomaly,
|
|
2174
|
+
correlationId: anomaly.correlationId ?? anomaly.requestId,
|
|
2173
2175
|
agentAid: anomaly.agentAid ?? session.selfAID ?? message.selfAID,
|
|
2174
2176
|
sessionId: anomaly.sessionId ?? session.id,
|
|
2175
2177
|
permissionMode: anomaly.permissionMode ?? effectivePermissionMode,
|
|
@@ -2758,6 +2760,12 @@ export class ResponseEngine {
|
|
|
2758
2760
|
this.handoffRuntime.completeTargetContext(selfAid, handoffId);
|
|
2759
2761
|
}
|
|
2760
2762
|
}
|
|
2763
|
+
// The runner may already own a stronger per-session directory (Codex
|
|
2764
|
+
// registers one before constructing its thread). Other runners use a
|
|
2765
|
+
// private child of the process-provided TMPDIR. Both paths are passed
|
|
2766
|
+
// explicitly so sandboxed helper commands never fall back to a shared
|
|
2767
|
+
// agent or project directory for transient state.
|
|
2768
|
+
const sessionRuntimeDir = agent.getManagedTempDir?.(session.id) ?? ensureSessionRuntimeDir(session.id);
|
|
2761
2769
|
const taskRuntimeContext = {
|
|
2762
2770
|
taskId,
|
|
2763
2771
|
sessionId: session.id,
|
|
@@ -2771,6 +2779,8 @@ export class ResponseEngine {
|
|
|
2771
2779
|
peerType: message.peerType || session.metadata?.peerType || undefined,
|
|
2772
2780
|
peerRole,
|
|
2773
2781
|
threadId: session.threadId || undefined,
|
|
2782
|
+
sessionRuntimeDir,
|
|
2783
|
+
runtimeLockDir: ensureRuntimeLockDir(sessionRuntimeDir),
|
|
2774
2784
|
handoffIds: v2HandoffIds.length > 0 && v2HandoffDirection === 'target' ? v2HandoffIds : undefined,
|
|
2775
2785
|
causation: taskCausation,
|
|
2776
2786
|
};
|
|
@@ -2841,7 +2851,7 @@ export class ResponseEngine {
|
|
|
2841
2851
|
const processAttempt = this.processEventStream(stream, session, agent, renderer, (eventType, toolName) => {
|
|
2842
2852
|
resetTimer(eventType, toolName);
|
|
2843
2853
|
attemptTimeout?.reset();
|
|
2844
|
-
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode);
|
|
2854
|
+
}, shouldSuppress, proactive, resolvedMode ? { mode: resolvedMode.mode, state: modeState } : undefined, taskCausation, turnLease, effectivePermissionMode, session.selfAID || message.selfAID);
|
|
2845
2855
|
streamResult = attemptTimeout
|
|
2846
2856
|
? await Promise.race([processAttempt, attemptTimeout.promise])
|
|
2847
2857
|
: await processAttempt;
|
|
@@ -2999,6 +3009,7 @@ export class ResponseEngine {
|
|
|
2999
3009
|
resetTimer,
|
|
3000
3010
|
shouldSuppress,
|
|
3001
3011
|
proactive,
|
|
3012
|
+
proactiveSelfAid: session.selfAID || message.selfAID,
|
|
3002
3013
|
permissionMode: effectivePermissionMode,
|
|
3003
3014
|
turnLease,
|
|
3004
3015
|
});
|
|
@@ -3041,6 +3052,7 @@ export class ResponseEngine {
|
|
|
3041
3052
|
resetTimer,
|
|
3042
3053
|
shouldSuppress,
|
|
3043
3054
|
proactive,
|
|
3055
|
+
proactiveSelfAid: session.selfAID || message.selfAID,
|
|
3044
3056
|
permissionMode: effectivePermissionMode,
|
|
3045
3057
|
turnLease,
|
|
3046
3058
|
});
|
|
@@ -4022,7 +4034,7 @@ export class ResponseEngine {
|
|
|
4022
4034
|
*/
|
|
4023
4035
|
async processEventStream(stream, session, agent, renderer, resetTimer, shouldSuppress, proactive,
|
|
4024
4036
|
/** [迁移点4/5] 响应模式插件 + 状态,用于调 onToolUse/onComplete 钩子 */
|
|
4025
|
-
modeHooks, causation, turnLease, permissionMode) {
|
|
4037
|
+
modeHooks, causation, turnLease, permissionMode, proactiveSelfAid) {
|
|
4026
4038
|
// Per-session agent name for stats bucketing
|
|
4027
4039
|
const statsChannelKey = session.channel === 'daemon' ? session.channel : (session.metadata?.channelKey || session.channel);
|
|
4028
4040
|
const agentNameForStats = this.agentRegistry?.resolveByChannel(statsChannelKey)?.name ?? '<unknown>';
|
|
@@ -4038,6 +4050,7 @@ export class ResponseEngine {
|
|
|
4038
4050
|
let toolUseCount = 0;
|
|
4039
4051
|
const openToolUseIds = new Set();
|
|
4040
4052
|
const anonymousToolUseIds = new Map();
|
|
4053
|
+
const anonymousOpenIdByCorrelation = new Map();
|
|
4041
4054
|
const finalizeResult = () => ({
|
|
4042
4055
|
...completeResult,
|
|
4043
4056
|
hasReceivedText,
|
|
@@ -4053,7 +4066,28 @@ export class ResponseEngine {
|
|
|
4053
4066
|
let lastReplyText = '';
|
|
4054
4067
|
// callId → description 映射,用于 tool_result 回显描述
|
|
4055
4068
|
const toolDescByCallId = new Map();
|
|
4069
|
+
const toolInputByCallId = new Map();
|
|
4070
|
+
const toolInputByCorrelationId = new Map();
|
|
4071
|
+
const anonymousToolInputsByName = new Map();
|
|
4056
4072
|
const ctlQueueReadCallIds = new Set();
|
|
4073
|
+
const removePendingToolInput = (pending) => {
|
|
4074
|
+
for (const [callId, value] of toolInputByCallId) {
|
|
4075
|
+
if (value === pending)
|
|
4076
|
+
toolInputByCallId.delete(callId);
|
|
4077
|
+
}
|
|
4078
|
+
for (const [correlationId, value] of toolInputByCorrelationId) {
|
|
4079
|
+
if (value === pending)
|
|
4080
|
+
toolInputByCorrelationId.delete(correlationId);
|
|
4081
|
+
}
|
|
4082
|
+
const anonymous = anonymousToolInputsByName.get(pending.name);
|
|
4083
|
+
if (anonymous) {
|
|
4084
|
+
const remaining = anonymous.filter(value => value !== pending);
|
|
4085
|
+
if (remaining.length > 0)
|
|
4086
|
+
anonymousToolInputsByName.set(pending.name, remaining);
|
|
4087
|
+
else
|
|
4088
|
+
anonymousToolInputsByName.delete(pending.name);
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4057
4091
|
try {
|
|
4058
4092
|
for await (let event of stream) {
|
|
4059
4093
|
if (turnLease && !this.turnCoordinator.isCurrent(turnLease)) {
|
|
@@ -4085,16 +4119,44 @@ export class ResponseEngine {
|
|
|
4085
4119
|
else {
|
|
4086
4120
|
const syntheticId = `__anonymous_tool_${toolUseCount}`;
|
|
4087
4121
|
openToolUseIds.add(syntheticId);
|
|
4122
|
+
if (event.correlationId)
|
|
4123
|
+
anonymousOpenIdByCorrelation.set(event.correlationId, syntheticId);
|
|
4088
4124
|
const pending = anonymousToolUseIds.get(event.name) ?? [];
|
|
4089
4125
|
pending.push(syntheticId);
|
|
4090
4126
|
anonymousToolUseIds.set(event.name, pending);
|
|
4091
4127
|
}
|
|
4092
4128
|
}
|
|
4093
4129
|
else if (event.type === 'tool_result') {
|
|
4094
|
-
|
|
4130
|
+
// A result carrying both identifiers may be stale or replayed. Do not
|
|
4131
|
+
// close the active call until both identifiers resolve to the same
|
|
4132
|
+
// pending tool invocation; otherwise a mismatched result can make the
|
|
4133
|
+
// protocol look complete even though the real call is still open.
|
|
4134
|
+
const hasBothResultIdentifiers = !!event.callId && !!event.correlationId;
|
|
4135
|
+
const byCallId = event.callId ? toolInputByCallId.get(event.callId) : undefined;
|
|
4136
|
+
const byCorrelationId = event.correlationId
|
|
4137
|
+
? toolInputByCorrelationId.get(event.correlationId)
|
|
4138
|
+
: undefined;
|
|
4139
|
+
const identifiersMatch = !hasBothResultIdentifiers
|
|
4140
|
+
|| (!!byCallId && !!byCorrelationId && byCallId === byCorrelationId);
|
|
4141
|
+
if (event.callId && identifiersMatch) {
|
|
4095
4142
|
openToolUseIds.delete(event.callId);
|
|
4096
4143
|
}
|
|
4097
|
-
else {
|
|
4144
|
+
else if (!event.callId && event.correlationId && byCorrelationId) {
|
|
4145
|
+
const syntheticId = anonymousOpenIdByCorrelation.get(event.correlationId);
|
|
4146
|
+
if (syntheticId) {
|
|
4147
|
+
openToolUseIds.delete(syntheticId);
|
|
4148
|
+
anonymousOpenIdByCorrelation.delete(event.correlationId);
|
|
4149
|
+
}
|
|
4150
|
+
else {
|
|
4151
|
+
for (const [callId, pending] of toolInputByCallId) {
|
|
4152
|
+
if (pending === byCorrelationId) {
|
|
4153
|
+
openToolUseIds.delete(callId);
|
|
4154
|
+
break;
|
|
4155
|
+
}
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
}
|
|
4159
|
+
else if (!event.callId) {
|
|
4098
4160
|
const names = event.name ? [event.name] : [...anonymousToolUseIds.keys()];
|
|
4099
4161
|
for (const name of names) {
|
|
4100
4162
|
const pending = anonymousToolUseIds.get(name);
|
|
@@ -4350,16 +4412,32 @@ export class ResponseEngine {
|
|
|
4350
4412
|
toolName: event.name,
|
|
4351
4413
|
input: event.input,
|
|
4352
4414
|
...(event.callId ? { callId: event.callId } : {}),
|
|
4415
|
+
...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
|
|
4353
4416
|
...(session.selfAID ? { agentAid: session.selfAID } : {}),
|
|
4354
4417
|
...(permissionMode ? { permissionMode } : {}),
|
|
4355
4418
|
timestamp: Date.now(),
|
|
4356
4419
|
causation,
|
|
4357
4420
|
});
|
|
4421
|
+
const desc = summarizeToolInput(event.name, event.input || {});
|
|
4422
|
+
const pendingInput = { name: event.name, input: event.input || {} };
|
|
4423
|
+
if (event.callId)
|
|
4424
|
+
toolInputByCallId.set(event.callId, pendingInput);
|
|
4425
|
+
if (event.correlationId)
|
|
4426
|
+
toolInputByCorrelationId.set(event.correlationId, pendingInput);
|
|
4427
|
+
else if (event.callId) {
|
|
4428
|
+
// Renderer/event-log normalization uses callId as the fallback
|
|
4429
|
+
// correlation. Keep the same alias here so a result carrying both
|
|
4430
|
+
// fields with that fallback still matches the original call.
|
|
4431
|
+
toolInputByCorrelationId.set(event.callId, pendingInput);
|
|
4432
|
+
}
|
|
4433
|
+
if (!event.callId && !event.correlationId) {
|
|
4434
|
+
const pending = anonymousToolInputsByName.get(event.name) ?? [];
|
|
4435
|
+
pending.push(pendingInput);
|
|
4436
|
+
anonymousToolInputsByName.set(event.name, pending);
|
|
4437
|
+
}
|
|
4358
4438
|
if (!shouldSuppress()) {
|
|
4359
|
-
|
|
4360
|
-
if (event.callId) {
|
|
4439
|
+
if (event.callId)
|
|
4361
4440
|
toolDescByCallId.set(event.callId, desc);
|
|
4362
|
-
}
|
|
4363
4441
|
renderer.addToolCall(event.name, event.input, event.callId, desc, event.turn, event.outputTokens);
|
|
4364
4442
|
}
|
|
4365
4443
|
if (event.callId && isCtlQueueReadCommand(event.name, event.input || {})) {
|
|
@@ -4375,7 +4453,7 @@ export class ResponseEngine {
|
|
|
4375
4453
|
toolInput: event.input || {},
|
|
4376
4454
|
injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
|
|
4377
4455
|
getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
|
|
4378
|
-
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', session.selfAID),
|
|
4456
|
+
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
|
|
4379
4457
|
logger,
|
|
4380
4458
|
});
|
|
4381
4459
|
}
|
|
@@ -4402,11 +4480,61 @@ export class ResponseEngine {
|
|
|
4402
4480
|
isError: event.isError,
|
|
4403
4481
|
agentName: agentNameForStats,
|
|
4404
4482
|
...(event.callId ? { callId: event.callId } : {}),
|
|
4483
|
+
...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
|
|
4405
4484
|
...(session.selfAID ? { agentAid: session.selfAID } : {}),
|
|
4406
4485
|
...(permissionMode ? { permissionMode } : {}),
|
|
4407
4486
|
timestamp: Date.now(),
|
|
4408
4487
|
causation,
|
|
4409
4488
|
});
|
|
4489
|
+
// A proactive send gate advances only after the matching command
|
|
4490
|
+
// completed successfully and returned an explicit send receipt.
|
|
4491
|
+
let original;
|
|
4492
|
+
const byCallId = event.callId ? toolInputByCallId.get(event.callId) : undefined;
|
|
4493
|
+
const byCorrelationId = event.correlationId
|
|
4494
|
+
? toolInputByCorrelationId.get(event.correlationId)
|
|
4495
|
+
: undefined;
|
|
4496
|
+
// Both identifiers must describe the same call. This avoids
|
|
4497
|
+
// accepting a stale/replayed correlation as the result of a new
|
|
4498
|
+
// call, or vice versa.
|
|
4499
|
+
const hasBothResultIdentifiers = !!event.callId && !!event.correlationId;
|
|
4500
|
+
if (hasBothResultIdentifiers
|
|
4501
|
+
&& (!byCallId || !byCorrelationId || byCallId !== byCorrelationId)) {
|
|
4502
|
+
logger.warn(`[ResponseEngine] Ignoring mismatched tool result identifiers: session=${session.id}`);
|
|
4503
|
+
}
|
|
4504
|
+
else if (!hasBothResultIdentifiers) {
|
|
4505
|
+
original = byCallId ?? byCorrelationId;
|
|
4506
|
+
}
|
|
4507
|
+
else {
|
|
4508
|
+
original = byCallId;
|
|
4509
|
+
}
|
|
4510
|
+
if (!original && !event.callId && !event.correlationId) {
|
|
4511
|
+
const pending = anonymousToolInputsByName.get(event.name);
|
|
4512
|
+
// Without either identifier, FIFO is unsafe when same-named calls
|
|
4513
|
+
// overlap or complete out of order. Leave the gate unchanged until
|
|
4514
|
+
// a unique pending call can be matched.
|
|
4515
|
+
if (pending?.length === 1) {
|
|
4516
|
+
original = pending[0];
|
|
4517
|
+
anonymousToolInputsByName.delete(event.name);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
if (original)
|
|
4521
|
+
removePendingToolInput(original);
|
|
4522
|
+
if (original && modeHooks?.mode?.onToolResult) {
|
|
4523
|
+
const proactiveState = modeHooks.state.get('proactive');
|
|
4524
|
+
await modeHooks.mode.onToolResult({
|
|
4525
|
+
session,
|
|
4526
|
+
state: modeHooks.state,
|
|
4527
|
+
toolName: original.name,
|
|
4528
|
+
toolInput: original.input,
|
|
4529
|
+
result: event.result,
|
|
4530
|
+
isError: event.isError,
|
|
4531
|
+
error: event.error,
|
|
4532
|
+
injectToModel: (text) => { agent.injectUserMessage?.(session.id, text); },
|
|
4533
|
+
getQueueLength: () => this.messageQueue?.getQueueLength(session.id) ?? 0,
|
|
4534
|
+
isSendCommand: (toolName, toolInput) => isExactEvolcoreSendCommandForSession(toolName, toolInput, session.channelId, proactiveState?.chatType === 'group' ? 'group' : 'private', proactiveSelfAid || session.selfAID),
|
|
4535
|
+
logger,
|
|
4536
|
+
});
|
|
4537
|
+
}
|
|
4410
4538
|
// 从 tool_use 阶段缓存的描述中回溯
|
|
4411
4539
|
const cachedDesc = event.callId ? toolDescByCallId.get(event.callId) : undefined;
|
|
4412
4540
|
if (event.isError && !shouldSuppress()) {
|