evolcore 0.0.16 → 0.0.18
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 +46 -0
- package/bin/codex-managed-hook.mjs +16 -7
- package/bin/install-codex-managed-hooks.mjs +4 -2
- package/dist/agents/claude-runner.js +132 -14
- package/dist/agents/codex-app-server-client.js +6 -1
- package/dist/agents/codex-runner.js +21 -6
- package/dist/agents/ecagent-runner.js +39 -10
- package/dist/agents/gemini-runner.js +90 -19
- package/dist/aun/aid/store.js +36 -0
- package/dist/aun/msg/group.js +3 -1
- package/dist/aun/msg/p2p.js +23 -9
- package/dist/channels/aun.js +159 -21
- package/dist/cli/agent-command.js +67 -6
- package/dist/cli/agent.js +26 -0
- package/dist/cli/command-log.js +23 -4
- package/dist/cli/daemon-commands.js +53 -12
- package/dist/cli/init.js +21 -5
- package/dist/cli/restart-monitor.js +13 -6
- package/dist/cli/task-context.js +46 -1
- package/dist/cli/watch-logs.js +2 -2
- package/dist/config/builtin-roles.js +5 -1
- package/dist/config/role-ranks.js +4 -0
- package/dist/core/audit/event-key.js +29 -0
- package/dist/core/audit/log-integrity.js +13 -3
- package/dist/core/auth/auth-gateway.js +14 -18
- package/dist/core/auth/authorization-audit.js +110 -3
- package/dist/core/auth/authorization-denial.js +17 -0
- package/dist/core/auth/operation-authorizer.js +143 -18
- package/dist/core/auth/operation-catalog.js +21 -5
- package/dist/core/bootstrap-messages.js +11 -6
- package/dist/core/bootstrap-service.js +26 -4
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +25 -16
- package/dist/core/command/command-handler.js +50 -4
- package/dist/core/command/group-menu.js +1 -1
- package/dist/core/command/menu-catalog.js +32 -7
- package/dist/core/command/menu-handler.js +59 -23
- package/dist/core/command/menu-protocol.js +196 -0
- package/dist/core/command/slash-gate.js +14 -5
- package/dist/core/command/slash-handler.js +81 -99
- package/dist/core/event-catalog.js +18 -0
- package/dist/core/message/message-bridge.js +72 -9
- package/dist/core/message/pause-controller.js +53 -0
- package/dist/core/message/response-engine.js +98 -11
- package/dist/core/permission/sandbox-runtime.js +79 -13
- package/dist/core/permission/tool-policy.js +1 -1
- package/dist/index.js +357 -48
- package/dist/ipc.js +75 -4
- package/dist/utils/atomic-write.js +45 -11
- package/dist/utils/error-utils.js +38 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/windows-autostart.js +740 -83
- package/ecagent/dist/harness/agent-harness.d.ts +1 -1
- package/ecagent/dist/harness/agent-harness.js +6 -4
- package/kits/docs/evolcore/config.md +1 -1
- package/kits/docs/evolcore/group-rules.md +2 -1
- package/kits/docs/identity/ROLE_DETAIL.md +3 -1
- package/kits/eck_manifest.json +25 -16
- package/kits/rules/01-overview.md +5 -5
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/04-relation.md +4 -4
- package/kits/rules/05-venue.md +5 -5
- package/kits/templates/bootstrap-welcome.md +3 -1
- package/kits/templates/system-fragments/bootstrap.md +17 -9
- package/package.json +1 -1
|
@@ -313,10 +313,16 @@ const CATALOG = [
|
|
|
313
313
|
{ path: 'sessionId', type: 'string' },
|
|
314
314
|
{ path: 'toolName', type: 'string' },
|
|
315
315
|
{ path: 'input', type: 'object' },
|
|
316
|
+
{ path: 'eventKey', type: 'string' },
|
|
317
|
+
{ path: 'eventPhase', type: 'string' },
|
|
316
318
|
{ path: 'callId', type: 'string', optional: true },
|
|
317
319
|
{ path: 'correlationId', type: 'string', optional: true },
|
|
318
320
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
319
321
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
322
|
+
{ path: 'decision', type: 'string' },
|
|
323
|
+
{ path: 'decisionSource', type: 'string' },
|
|
324
|
+
{ path: 'executed', type: 'boolean' },
|
|
325
|
+
{ path: 'executionState', type: 'string' },
|
|
320
326
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
321
327
|
],
|
|
322
328
|
},
|
|
@@ -328,6 +334,8 @@ const CATALOG = [
|
|
|
328
334
|
fields: [
|
|
329
335
|
{ path: 'sessionId', type: 'string' },
|
|
330
336
|
{ path: 'toolName', type: 'string' },
|
|
337
|
+
{ path: 'eventKey', type: 'string' },
|
|
338
|
+
{ path: 'eventPhase', type: 'string' },
|
|
331
339
|
{ path: 'isError', type: 'boolean', optional: true },
|
|
332
340
|
{ path: 'errorCode', type: 'string', optional: true },
|
|
333
341
|
{ path: 'agentName', type: 'string', optional: true },
|
|
@@ -335,6 +343,10 @@ const CATALOG = [
|
|
|
335
343
|
{ path: 'correlationId', type: 'string', optional: true },
|
|
336
344
|
{ path: 'agentAid', type: 'string', optional: true },
|
|
337
345
|
{ path: 'permissionMode', type: 'string', optional: true },
|
|
346
|
+
{ path: 'decision', type: 'string' },
|
|
347
|
+
{ path: 'decisionSource', type: 'string' },
|
|
348
|
+
{ path: 'executed', type: 'boolean' },
|
|
349
|
+
{ path: 'executionState', type: 'string' },
|
|
338
350
|
{ path: 'timestamp', type: 'number', optional: true },
|
|
339
351
|
],
|
|
340
352
|
},
|
|
@@ -846,3 +858,9 @@ export function getEventCatalog(opts = {}) {
|
|
|
846
858
|
const namespaces = [...new Set(events.map(entry => entry.namespace))].sort();
|
|
847
859
|
return { namespaces, events };
|
|
848
860
|
}
|
|
861
|
+
/** Whether an event Trigger pattern can subscribe to daemon-internal events. */
|
|
862
|
+
export function eventPatternIncludesInternal(pattern) {
|
|
863
|
+
return CATALOG.some(entry => entry.internal === true && (pattern === '*'
|
|
864
|
+
|| pattern === entry.type
|
|
865
|
+
|| (pattern.endsWith(':*') && entry.namespace === pattern.slice(0, -2))));
|
|
866
|
+
}
|
|
@@ -17,7 +17,7 @@ import { handlePendingQQBotContactBindMessage, registerPendingQQBotContactBind }
|
|
|
17
17
|
import { handlePendingWecomContactBindMessage, registerPendingWecomContactBind } from '../../channels/wecom.js';
|
|
18
18
|
import { handlePendingWechatContactBindMessage, registerPendingWechatContactBind } from '../../channels/wechat.js';
|
|
19
19
|
import { authorizeAccess, buildAuthSubject } from '../auth/auth-gateway.js';
|
|
20
|
-
import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, } from '../command/menu-protocol.js';
|
|
20
|
+
import { MenuDiagnosticLimiter, MenuRequestDeduper, hasValidMenuId, menuFailure, menuPayloadFingerprint, menuSuccess, normalizeMenuError, parseMenuControl, validateMenuRequest, evaluateEvolMenuVersionGate, evolMenuResponseTransportMetadata, isAunMenuTokenRequired, menuCommandForName, normalizeMenuResponseTiming, withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived, } from '../command/menu-protocol.js';
|
|
21
21
|
import { resolveExplicitMenuExecutionContext, validateMenuCatalogArgs, validateMenuValueBatchArgs } from '../command/menu-catalog.js';
|
|
22
22
|
import { MenuTokenStore } from '../command/menu-token-store.js';
|
|
23
23
|
import { SessionRenewService } from '../session/session-renew.js';
|
|
@@ -691,9 +691,16 @@ export class MessageBridge {
|
|
|
691
691
|
// that timestamp through session resolution and queue processing.
|
|
692
692
|
// The fallback is only for legacy adapters that do not provide it.
|
|
693
693
|
const receivedAt = msg.receivedAt ?? Date.now();
|
|
694
|
+
const menuTiming = normalizeMenuResponseTiming(msg);
|
|
694
695
|
const channelKey = adapter?.channelKey || channelName;
|
|
695
696
|
const owningAgent = this.agentRegistry?.resolveByChannel(channelKey)
|
|
696
697
|
?? this.agentRegistry?.resolveByChannel(channelName);
|
|
698
|
+
// Agent-owned business controls bypass ResponseEngine entirely, so
|
|
699
|
+
// expose them only after bootstrap has explicitly reached active.
|
|
700
|
+
// Missing lifecycle fails closed; control channels without an owning
|
|
701
|
+
// EvolAgent keep their existing command behavior.
|
|
702
|
+
const allowsNormalCommandRouting = !owningAgent
|
|
703
|
+
|| owningAgent.config?.lifecycle === 'active';
|
|
697
704
|
const parsedChannelKey = tryParseChannelKey(channelKey);
|
|
698
705
|
const chatType = msg.chatType || 'private';
|
|
699
706
|
const delivery = chatType === 'group'
|
|
@@ -772,14 +779,21 @@ export class MessageBridge {
|
|
|
772
779
|
logger.info(`[MessageBridge] Inbound admission denied before business routing: self=${selfAid} actor=${actorId ?? '<none>'} channel=${channelKey} reason=${admissionDecision.reason}`);
|
|
773
780
|
return;
|
|
774
781
|
}
|
|
775
|
-
const menuControl = resolvedChannelType === 'aun'
|
|
782
|
+
const menuControl = allowsNormalCommandRouting && resolvedChannelType === 'aun'
|
|
776
783
|
? parseMenuControl(content)
|
|
777
784
|
: { isMenu: false };
|
|
778
785
|
let menuResponseWarning;
|
|
779
786
|
if (menuControl.isMenu) {
|
|
787
|
+
// AUN captures this mark at its adapter boundary. Legacy/test
|
|
788
|
+
// adapters fall back to the bridge entry mark captured above.
|
|
789
|
+
Object.assign(msg, menuTiming);
|
|
780
790
|
this.logMenuInbound(channelName, msg, menuControl);
|
|
781
791
|
if (!hasValidMenuId(menuControl)) {
|
|
782
792
|
this.logMenuDiagnostic('missing-id', channelName, msg, menuControl);
|
|
793
|
+
logMenuRequestCompleted(menuControl.request, undefined, this.menuFlowContext(channelName, msg), {
|
|
794
|
+
delivery: 'dropped',
|
|
795
|
+
reason: 'missing-id',
|
|
796
|
+
});
|
|
783
797
|
return;
|
|
784
798
|
}
|
|
785
799
|
const versionError = evaluateEvolMenuVersionGate({
|
|
@@ -916,7 +930,9 @@ export class MessageBridge {
|
|
|
916
930
|
// 2. 命令快速路径(去除引用前缀后检查,兼容话题中引用上文的情况)
|
|
917
931
|
const contentForCmd = content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
|
|
918
932
|
const cmdContent = contentForCmd || content;
|
|
919
|
-
const isCmd =
|
|
933
|
+
const isCmd = allowsNormalCommandRouting
|
|
934
|
+
&& msg.source !== 'handoff'
|
|
935
|
+
&& this.cmdHandler.isCommand(cmdContent);
|
|
920
936
|
if (isCmd) {
|
|
921
937
|
logger.debug(`[MessageBridge] Command detected: "${cmdContent}", routing to handler`);
|
|
922
938
|
// 命令也要记录入方向 jsonl(不创建 session,直接用 chatDirPath 计算路径)
|
|
@@ -953,11 +969,13 @@ export class MessageBridge {
|
|
|
953
969
|
logger.debug(`[MessageBridge] Failed to log inbound command: ${e}`);
|
|
954
970
|
}
|
|
955
971
|
}
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
972
|
+
if (allowsNormalCommandRouting
|
|
973
|
+
&& msg.source !== 'handoff'
|
|
974
|
+
&& await this.handleCommand(cmdContent, channelName, msg.channelId, (text) => {
|
|
975
|
+
const taskId = `cmd-${msg.messageId || Date.now()}`;
|
|
976
|
+
logger.channelOut({ channel: channelName, channelId: msg.channelId, taskId, correlationId: taskId, sessionId: msg.replyContext?.sessionId, agentAid: msg.selfAID, payload: { kind: 'command.result', text } });
|
|
977
|
+
return sendReply(msg.channelId, text, msg.replyContext);
|
|
978
|
+
}, msg.peerId, msg.threadId, msg.chatType, msg.source, msg.replyContext, msg.messageId, msg.selfAID, authSubject))
|
|
961
979
|
return;
|
|
962
980
|
// 3. session 解析(使用 Channel 层填充的 chatType)
|
|
963
981
|
if (!(await this.canCreateThreadSession(channelName, msg, chatType))) {
|
|
@@ -1408,6 +1426,9 @@ export class MessageBridge {
|
|
|
1408
1426
|
}
|
|
1409
1427
|
}
|
|
1410
1428
|
async sendMenuResponse(adapter, channel, msg, response) {
|
|
1429
|
+
const request = this.menuRequestForLog(msg, response);
|
|
1430
|
+
const flowContext = this.menuFlowContext(channel, msg);
|
|
1431
|
+
const timedResponse = withMenuProcessingTime(response, msg);
|
|
1411
1432
|
if (!adapter?.send) {
|
|
1412
1433
|
this.logMenuDiagnostic('transport-unavailable', channel, msg, {
|
|
1413
1434
|
isMenu: true,
|
|
@@ -1417,6 +1438,10 @@ export class MessageBridge {
|
|
|
1417
1438
|
id: response.id,
|
|
1418
1439
|
name: response.name,
|
|
1419
1440
|
}, 'TEMPORARILY_UNAVAILABLE');
|
|
1441
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, {
|
|
1442
|
+
delivery: 'dropped',
|
|
1443
|
+
reason: 'transport-unavailable',
|
|
1444
|
+
});
|
|
1420
1445
|
return;
|
|
1421
1446
|
}
|
|
1422
1447
|
const agentName = this.agentRegistry?.resolveByChannel(channel)?.name ?? '<unknown>';
|
|
@@ -1429,6 +1454,10 @@ export class MessageBridge {
|
|
|
1429
1454
|
};
|
|
1430
1455
|
if (msg.channelType === 'aun' && !replyContext.delivery) {
|
|
1431
1456
|
logger.warn(`[MessageBridge] dropping AUN menu response without trusted delivery route: channel=${channel} channelId=${msg.channelId}`);
|
|
1457
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, {
|
|
1458
|
+
delivery: 'dropped',
|
|
1459
|
+
reason: 'missing-delivery-route',
|
|
1460
|
+
});
|
|
1432
1461
|
return;
|
|
1433
1462
|
}
|
|
1434
1463
|
const envelope = buildEnvelope({
|
|
@@ -1438,7 +1467,18 @@ export class MessageBridge {
|
|
|
1438
1467
|
agentName,
|
|
1439
1468
|
replyContext,
|
|
1440
1469
|
});
|
|
1441
|
-
|
|
1470
|
+
try {
|
|
1471
|
+
await adapter.send(envelope, { kind: 'custom', channelType: channel, payload: timedResponse });
|
|
1472
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, { delivery: 'sent' });
|
|
1473
|
+
}
|
|
1474
|
+
catch (error) {
|
|
1475
|
+
logMenuRequestCompleted(request, timedResponse, flowContext, {
|
|
1476
|
+
delivery: 'failed',
|
|
1477
|
+
reason: 'transport-error',
|
|
1478
|
+
transportError: error,
|
|
1479
|
+
});
|
|
1480
|
+
throw error;
|
|
1481
|
+
}
|
|
1442
1482
|
}
|
|
1443
1483
|
menuTokenOwnerAid(channel, msg) {
|
|
1444
1484
|
return msg.selfAID || this.agentRegistry?.resolveByChannel(channel)?.aid;
|
|
@@ -1466,6 +1506,7 @@ export class MessageBridge {
|
|
|
1466
1506
|
return normalized.length > 0 && normalized.length <= 256 ? normalized : undefined;
|
|
1467
1507
|
}
|
|
1468
1508
|
logMenuInbound(channel, msg, parsed) {
|
|
1509
|
+
logMenuRequestReceived(parsed.request, this.menuFlowContext(channel, msg));
|
|
1469
1510
|
logger.channelIn({
|
|
1470
1511
|
channel,
|
|
1471
1512
|
channelId: this.shortHash(msg.channelId),
|
|
@@ -1482,6 +1523,21 @@ export class MessageBridge {
|
|
|
1482
1523
|
},
|
|
1483
1524
|
});
|
|
1484
1525
|
}
|
|
1526
|
+
menuRequestForLog(msg, response) {
|
|
1527
|
+
try {
|
|
1528
|
+
const parsed = JSON.parse(msg.content);
|
|
1529
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
1530
|
+
return parsed;
|
|
1531
|
+
}
|
|
1532
|
+
catch {
|
|
1533
|
+
// Fall through to a minimal correlation record.
|
|
1534
|
+
}
|
|
1535
|
+
return {
|
|
1536
|
+
type: 'menu.unknown',
|
|
1537
|
+
id: response.id,
|
|
1538
|
+
...(response.name ? { name: response.name } : {}),
|
|
1539
|
+
};
|
|
1540
|
+
}
|
|
1485
1541
|
logMenuDiagnostic(category, channel, msg, parsed, code) {
|
|
1486
1542
|
const scope = msg.chatType === 'group' ? (msg.groupId || msg.channelId) : msg.peerId;
|
|
1487
1543
|
const key = `${category}:${channel}:${this.shortHash(scope)}`;
|
|
@@ -1611,6 +1667,13 @@ export class MessageBridge {
|
|
|
1611
1667
|
* 撤回消息:先查 debounce 窗口,再查 message queue,最后查正在执行的任务。
|
|
1612
1668
|
* @returns true 如果找到并取消/中断
|
|
1613
1669
|
*/
|
|
1670
|
+
menuFlowContext(channel, msg) {
|
|
1671
|
+
return {
|
|
1672
|
+
source: 'agent-aun',
|
|
1673
|
+
selfAid: msg.selfAID ?? this.agentRegistry?.resolveByChannel(channel)?.aid,
|
|
1674
|
+
messageId: msg.messageId,
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1614
1677
|
cancel(messageId, selfAID) {
|
|
1615
1678
|
// 阶段 1: debounce 窗口(尚未入队)
|
|
1616
1679
|
for (const d of this.debouncers.values()) {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/** Per-session gate used only at the next tool boundary. */
|
|
2
|
+
export class PauseController {
|
|
3
|
+
state = 'running';
|
|
4
|
+
waiters = new Set();
|
|
5
|
+
requestPause() {
|
|
6
|
+
if (this.state !== 'running')
|
|
7
|
+
return false;
|
|
8
|
+
this.state = 'pause_requested';
|
|
9
|
+
return true;
|
|
10
|
+
}
|
|
11
|
+
isPauseRequested() { return this.state === 'pause_requested'; }
|
|
12
|
+
isPaused() { return this.state === 'paused'; }
|
|
13
|
+
waitAtToolBoundary(signal) {
|
|
14
|
+
if (signal?.aborted)
|
|
15
|
+
return Promise.resolve('cancelled');
|
|
16
|
+
if (this.state === 'running')
|
|
17
|
+
return Promise.resolve('none');
|
|
18
|
+
this.state = 'paused';
|
|
19
|
+
return new Promise(resolve => {
|
|
20
|
+
const waiter = { resolve: () => { }, abort: undefined };
|
|
21
|
+
const finish = (result) => {
|
|
22
|
+
this.waiters.delete(waiter);
|
|
23
|
+
if (waiter.abort)
|
|
24
|
+
signal?.removeEventListener('abort', waiter.abort);
|
|
25
|
+
resolve(result);
|
|
26
|
+
};
|
|
27
|
+
waiter.resolve = finish;
|
|
28
|
+
if (signal) {
|
|
29
|
+
waiter.abort = () => finish('cancelled');
|
|
30
|
+
signal.addEventListener('abort', waiter.abort, { once: true });
|
|
31
|
+
}
|
|
32
|
+
this.waiters.add(waiter);
|
|
33
|
+
if (this.state === 'running')
|
|
34
|
+
finish('released');
|
|
35
|
+
else if (signal?.aborted)
|
|
36
|
+
finish('cancelled');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
resume() {
|
|
40
|
+
if (this.state === 'running')
|
|
41
|
+
return false;
|
|
42
|
+
this.state = 'running';
|
|
43
|
+
for (const waiter of [...this.waiters])
|
|
44
|
+
waiter.resolve('released');
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
cancel() {
|
|
48
|
+
this.state = 'running';
|
|
49
|
+
for (const waiter of [...this.waiters])
|
|
50
|
+
waiter.resolve('cancelled');
|
|
51
|
+
}
|
|
52
|
+
clear() { this.cancel(); }
|
|
53
|
+
}
|
|
@@ -13,7 +13,7 @@ import { isHostChinese } from '../../utils/locale.js';
|
|
|
13
13
|
import { getErrorMessage, classifyError, ErrorType, ERROR_PREFIX, isInfraError, isAbortTerminalReason, prefixErrorType, isRetryableError, isContextTooLongText } from '../../utils/error-utils.js';
|
|
14
14
|
import { isExactEvolcoreSendCommandForSession } from '../permission/ec-command-parser.js';
|
|
15
15
|
import { summarizeToolInput } from '../../utils/tool-summary.js';
|
|
16
|
-
import { getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
16
|
+
import { agentDir as resolveAgentDir, agentMdPath, getPackageRoot, resolveRoot, resolvePaths } from '../../paths.js';
|
|
17
17
|
import { renderKitSections } from '../../eck/kit-renderer.js';
|
|
18
18
|
import { renderMessageBody } from '../../eck/message-renderer.js';
|
|
19
19
|
import { syncGroupRulesContext } from '../../eck/group-rules-sync.js';
|
|
@@ -47,6 +47,8 @@ import { deriveSessionTitle, shouldAutoFillSessionTitle } from '../session/sessi
|
|
|
47
47
|
import { SessionTurnCoordinator } from '../session/session-turn-coordinator.js';
|
|
48
48
|
import { recordTriggerExecutionAnomaly } from '../../trigger/anomaly-store.js';
|
|
49
49
|
import { classifyToolErrorCode } from '../permission/tool-error-code.js';
|
|
50
|
+
import { buildToolLifecycleEventKey } from '../audit/event-key.js';
|
|
51
|
+
import { PauseController } from './pause-controller.js';
|
|
50
52
|
function isShowActivitiesMode(value) {
|
|
51
53
|
return value === 'all' || value === 'text' || value === 'none';
|
|
52
54
|
}
|
|
@@ -592,6 +594,28 @@ export class ResponseEngine {
|
|
|
592
594
|
registerBuiltinModes(registry);
|
|
593
595
|
this.responseCoordinator = new ResponseModeCoordinator(registry);
|
|
594
596
|
}
|
|
597
|
+
pauseControllers = new Map();
|
|
598
|
+
getPauseController(sessionId) {
|
|
599
|
+
let controller = this.pauseControllers.get(sessionId);
|
|
600
|
+
if (!controller) {
|
|
601
|
+
controller = new PauseController();
|
|
602
|
+
this.pauseControllers.set(sessionId, controller);
|
|
603
|
+
}
|
|
604
|
+
return controller;
|
|
605
|
+
}
|
|
606
|
+
pauseSession(sessionId) { return this.getPauseController(sessionId).requestPause(); }
|
|
607
|
+
resumeSession(sessionId) { return this.pauseControllers.get(sessionId)?.resume() ?? false; }
|
|
608
|
+
isPauseRequested(sessionId) {
|
|
609
|
+
const controller = this.pauseControllers.get(sessionId);
|
|
610
|
+
return !!controller && (controller.isPauseRequested() || controller.isPaused());
|
|
611
|
+
}
|
|
612
|
+
clearPauseSession(sessionId, expected) {
|
|
613
|
+
const controller = this.pauseControllers.get(sessionId);
|
|
614
|
+
if (expected && controller !== expected)
|
|
615
|
+
return;
|
|
616
|
+
controller?.clear();
|
|
617
|
+
this.pauseControllers.delete(sessionId);
|
|
618
|
+
}
|
|
595
619
|
setInteractionRouter(router) {
|
|
596
620
|
this.interactionRouter = router;
|
|
597
621
|
// 等待用户交互期间暂停 idle 监控,应答/取消/超时后恢复——
|
|
@@ -609,6 +633,7 @@ export class ResponseEngine {
|
|
|
609
633
|
this.messageQueue = queue;
|
|
610
634
|
}
|
|
611
635
|
async interruptSession(sessionId, reason) {
|
|
636
|
+
this.clearPauseSession(sessionId);
|
|
612
637
|
// A retry delay has no active runner stream to abort. Wake it before any
|
|
613
638
|
// session-store I/O so /stop and a newer message take effect immediately.
|
|
614
639
|
this.cancelRetryDelays(sessionId);
|
|
@@ -1165,7 +1190,7 @@ export class ResponseEngine {
|
|
|
1165
1190
|
static COMMAND_PREFIXES = [
|
|
1166
1191
|
'/new', '/pwd', '/help', '/status', '/restart',
|
|
1167
1192
|
'/model', '/effort', '/agent', '/slist', '/session', '/rename', '/repair', '/fork',
|
|
1168
|
-
'/stop', '/clear', '/compact', '/del', '/perm', '/file', '/check',
|
|
1193
|
+
'/stop', '/pause', '/resume', '/clear', '/compact', '/del', '/perm', '/file', '/check',
|
|
1169
1194
|
'/s ', '/name ', '/rewind', '/rw', '/rw ', '/activity', '/observable', '/chatmode',
|
|
1170
1195
|
'/aid', '/upgrade', '/evolagent',
|
|
1171
1196
|
];
|
|
@@ -1293,7 +1318,10 @@ export class ResponseEngine {
|
|
|
1293
1318
|
// 按 session.baseagent 选择 agent 后端(idle-kill 路径需要 interrupt)
|
|
1294
1319
|
let agent;
|
|
1295
1320
|
try {
|
|
1296
|
-
|
|
1321
|
+
const runnerSelfAid = this.agentRegistry?.resolveByChannel(channelKey)?.aid
|
|
1322
|
+
|| session.selfAID
|
|
1323
|
+
|| message.selfAID;
|
|
1324
|
+
agent = this.getAgent(channelKey, session.baseagent, runnerSelfAid);
|
|
1297
1325
|
}
|
|
1298
1326
|
catch (error) {
|
|
1299
1327
|
if (error instanceof BaseagentRunnerUnavailableError) {
|
|
@@ -1526,6 +1554,10 @@ export class ResponseEngine {
|
|
|
1526
1554
|
const taskAgentAid = message.selfAID || session.selfAID;
|
|
1527
1555
|
const owningAgentForTask = this.agentRegistry?.resolveByChannel(channelKey)
|
|
1528
1556
|
?? (taskAgentAid ? this.agentRegistry?.get(taskAgentAid) : null);
|
|
1557
|
+
const runnerSelfAid = owningAgentForTask?.aid || session.selfAID || message.selfAID;
|
|
1558
|
+
const lifecycle = owningAgentForTask?.config?.lifecycle;
|
|
1559
|
+
const isActive = lifecycle === 'active';
|
|
1560
|
+
const isBootstrapping = lifecycle === 'bootstrapping';
|
|
1529
1561
|
// Per-method agent name for stats bucketing (agent.name or '<unknown>')
|
|
1530
1562
|
const agentNameForStats = owningAgentForTask?.name ?? taskAgentAid ?? '<unknown>';
|
|
1531
1563
|
if (!channelInfo) {
|
|
@@ -1533,10 +1565,19 @@ export class ResponseEngine {
|
|
|
1533
1565
|
this.publishTriggerExecutionFailure(message, `unknown_channel:${channelKey}`);
|
|
1534
1566
|
return;
|
|
1535
1567
|
}
|
|
1568
|
+
// Agent-owned turns are runnable only in the two explicit execution
|
|
1569
|
+
// states. An untransitioned `created` agent or a malformed/missing
|
|
1570
|
+
// lifecycle must not fall through to a base runner's default prompt.
|
|
1571
|
+
if (owningAgentForTask && !isActive && !isBootstrapping) {
|
|
1572
|
+
const blockedLifecycle = lifecycle ?? 'missing';
|
|
1573
|
+
logger.error(`[ResponseEngine] Agent lifecycle is not runnable: agent=${owningAgentForTask.aid} lifecycle=${blockedLifecycle}`);
|
|
1574
|
+
this.publishTriggerExecutionFailure(message, `agent_lifecycle_not_runnable:${blockedLifecycle}`);
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1536
1577
|
// 二次拦截:如果命令消息绕过 MessageBridge 的 handleCommand 泄漏到这里,
|
|
1537
1578
|
// 静默丢弃而不是发送给 Agent(命令已在 MessageBridge 层处理过)
|
|
1538
1579
|
const rawContent = message.content.replace(/^(>[^\n]*\n)+\n?/, '').trim();
|
|
1539
|
-
if (rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
|
|
1580
|
+
if (!isBootstrapping && rawContent.startsWith('/') && this.isKnownCommand(rawContent)) {
|
|
1540
1581
|
logger.warn(`[ResponseEngine] Command leaked past MessageBridge, dropped: "${rawContent.substring(0, 40)}"`);
|
|
1541
1582
|
this.publishTriggerExecutionFailure(message, 'trigger_command_not_supported');
|
|
1542
1583
|
return;
|
|
@@ -1546,7 +1587,7 @@ export class ResponseEngine {
|
|
|
1546
1587
|
const identityRole = session.identity?.role || 'none';
|
|
1547
1588
|
let agent;
|
|
1548
1589
|
try {
|
|
1549
|
-
agent = this.getAgent(channelKey, session.baseagent,
|
|
1590
|
+
agent = this.getAgent(channelKey, session.baseagent, runnerSelfAid);
|
|
1550
1591
|
}
|
|
1551
1592
|
catch (error) {
|
|
1552
1593
|
if (error instanceof BaseagentRunnerUnavailableError) {
|
|
@@ -1569,6 +1610,7 @@ export class ResponseEngine {
|
|
|
1569
1610
|
}
|
|
1570
1611
|
// 为本次任务处理生成唯一 task_id(客户端生成,格式 task-{10hex})
|
|
1571
1612
|
const taskId = `task-${crypto.randomUUID().replace(/-/g, '').slice(0, 10)}`;
|
|
1613
|
+
const taskPauseController = this.getPauseController(session.id);
|
|
1572
1614
|
let turnLease;
|
|
1573
1615
|
const inputCausation = normalizeCausation(message.causation) ?? createRootCausation();
|
|
1574
1616
|
const taskCausation = deriveCausation(inputCausation);
|
|
@@ -1604,7 +1646,12 @@ export class ResponseEngine {
|
|
|
1604
1646
|
const currentChannelType = options?.channelType || message.channel;
|
|
1605
1647
|
const adapterAny = channelInfo.adapter;
|
|
1606
1648
|
const adapterSelfAid = typeof adapterAny._selfAid === 'function' ? adapterAny._selfAid() : undefined;
|
|
1607
|
-
|
|
1649
|
+
// During bootstrap the lifecycle owner is the authoritative identity. Do
|
|
1650
|
+
// not let a stale/crafted message or session selfAID redirect agent.md
|
|
1651
|
+
// access or the two lifecycle commands to another local AID.
|
|
1652
|
+
const selfAid = isBootstrapping
|
|
1653
|
+
? owningAgentForTask?.aid
|
|
1654
|
+
: adapterSelfAid || message.selfAID || session.selfAID || undefined;
|
|
1608
1655
|
const registrySelfName = selfAid ? this.agentRegistry?.resolveDisplayName?.(selfAid) : undefined;
|
|
1609
1656
|
const adapterSelfName = typeof adapterAny._selfName === 'function' ? adapterAny._selfName() : undefined;
|
|
1610
1657
|
const selfName = registrySelfName ?? adapterSelfName;
|
|
@@ -1684,9 +1731,10 @@ export class ResponseEngine {
|
|
|
1684
1731
|
chatType,
|
|
1685
1732
|
peerType,
|
|
1686
1733
|
});
|
|
1734
|
+
const responseModeAgentDir = selfAid ? resolveAgentDir(selfAid) : undefined;
|
|
1687
1735
|
// mentionMode:顶层通用参数(先读出备用,投递/入队策略后续接入)
|
|
1688
1736
|
const mentionMode = effectiveAgentConfig?.mentionMode ?? 'disabled';
|
|
1689
|
-
const resolvedMode = triggerChatModeOverride || systemOrServicePeer
|
|
1737
|
+
const resolvedMode = triggerChatModeOverride || systemOrServicePeer || !responseModeAgentDir
|
|
1690
1738
|
? null // trigger 强制覆盖或 system/service 强制 interactive 时,不走插件解析
|
|
1691
1739
|
: this.responseCoordinator.resolveMode(effectiveAgentConfig?.responseMode, // 标量:关系级>agent级>注册表首选
|
|
1692
1740
|
chatModeFallback, effectiveAgentConfig?.responseModeParams, // 按模式分桶的参数字典
|
|
@@ -1706,7 +1754,7 @@ export class ResponseEngine {
|
|
|
1706
1754
|
send: async () => { }, // 引擎自行发送,插件 handleOutbound 只做决策
|
|
1707
1755
|
},
|
|
1708
1756
|
logger,
|
|
1709
|
-
agentDir:
|
|
1757
|
+
agentDir: responseModeAgentDir,
|
|
1710
1758
|
});
|
|
1711
1759
|
if (resolvedMode) {
|
|
1712
1760
|
logger.info('[ResponseSystem] selected mode=' + resolvedMode.mode.id + ' source=' + resolvedMode.source + ' chatType=' + chatType + ' peerKey=' + (peerKey ?? 'none') + ' chatMode=' + chatModeFallback);
|
|
@@ -2208,6 +2256,7 @@ export class ResponseEngine {
|
|
|
2208
2256
|
approvalRouting,
|
|
2209
2257
|
approvalInteractionPolicy: authorizationIdentity ? 'deny' : 'interactive',
|
|
2210
2258
|
recordExecutionAnomaly,
|
|
2259
|
+
pauseController: taskPauseController,
|
|
2211
2260
|
preToolUsePolicyHook: pureSessionPolicyHook,
|
|
2212
2261
|
armApprovedDelegationCommand: this.agentDelegationRegistry
|
|
2213
2262
|
? (carrierToken, commandHash) => this.agentDelegationRegistry.armApprovedCommand({
|
|
@@ -2363,8 +2412,8 @@ export class ResponseEngine {
|
|
|
2363
2412
|
// Personal state is exposed to the manifest as vars. The manifest owns
|
|
2364
2413
|
// loading persona.md and working.md into the system prompt.
|
|
2365
2414
|
const owningAgent = owningAgentForTask;
|
|
2366
|
-
const persona = owningAgent?.getPersona?.() || undefined;
|
|
2367
|
-
const working = owningAgent?.getWorkingMemory?.() || undefined;
|
|
2415
|
+
const persona = isActive ? (owningAgent?.getPersona?.() || undefined) : undefined;
|
|
2416
|
+
const working = isActive ? (owningAgent?.getWorkingMemory?.() || undefined) : undefined;
|
|
2368
2417
|
// 计算 peerKey:群聊固定按 groupId/channelId,私聊按发送者 peerId。
|
|
2369
2418
|
// 这样单条和积压合并批次不会因队列状态不同而切换关系级配置。
|
|
2370
2419
|
const normalizedBaseagent = normalizeBaseagent(agent.name);
|
|
@@ -2521,6 +2570,12 @@ export class ResponseEngine {
|
|
|
2521
2570
|
if (shouldPassPermissionMode) {
|
|
2522
2571
|
modelOverride = { ...(modelOverride || {}), permissionMode: effectivePermissionMode };
|
|
2523
2572
|
}
|
|
2573
|
+
if (isBootstrapping && selfAid) {
|
|
2574
|
+
modelOverride = {
|
|
2575
|
+
...(modelOverride || {}),
|
|
2576
|
+
bootstrapAgentMdPath: agentMdPath(selfAid),
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2524
2579
|
if (normalizedBaseagent.canonical === 'claude') {
|
|
2525
2580
|
modelOverride = {
|
|
2526
2581
|
...(modelOverride || {}),
|
|
@@ -2567,6 +2622,9 @@ export class ResponseEngine {
|
|
|
2567
2622
|
VENUES_DIR: selfAid ? path.join(resolveRoot(), 'agents', selfAid, 'venues') : undefined,
|
|
2568
2623
|
selfAid: selfAid || undefined,
|
|
2569
2624
|
selfName: selfName || undefined,
|
|
2625
|
+
agentMdPath: selfAid ? agentMdPath(selfAid) : undefined,
|
|
2626
|
+
lifecycle,
|
|
2627
|
+
isBootstrapping,
|
|
2570
2628
|
hasPersona: !!persona,
|
|
2571
2629
|
hasWorkingMemory: !!working,
|
|
2572
2630
|
peerId: peerIdRaw || undefined,
|
|
@@ -2637,7 +2695,12 @@ export class ResponseEngine {
|
|
|
2637
2695
|
// 按会话原型(sessionType)选 manifest 文件:config.sessionManifests 映射,缺省回退主 manifest。
|
|
2638
2696
|
const sessionType = session.sessionType ?? 'main';
|
|
2639
2697
|
const sessionManifests = this.agentRegistry?.resolveByChannel(channelKey)?.config?.sessionManifests;
|
|
2640
|
-
|
|
2698
|
+
// Bootstrap always uses the lifecycle-gated main manifest. A custom
|
|
2699
|
+
// session manifest (for example the auxiliary manifest with `always`
|
|
2700
|
+
// sections) must not reintroduce normal rules or command context.
|
|
2701
|
+
const manifestFile = isBootstrapping
|
|
2702
|
+
? 'eck_manifest.json'
|
|
2703
|
+
: sessionManifests?.[sessionType] ?? 'eck_manifest.json';
|
|
2641
2704
|
const kitContext = renderKitSections(kitCtx, manifestFile);
|
|
2642
2705
|
effectiveSystemPrompt = [options?.systemPromptAppend, kitContext].filter(Boolean).join('\n') || undefined;
|
|
2643
2706
|
// ── Stats: context_breakdown 旁路采集(各段估算 token 数,字符数/4 近似) ──
|
|
@@ -2775,6 +2838,7 @@ export class ResponseEngine {
|
|
|
2775
2838
|
taskId,
|
|
2776
2839
|
sessionId: session.id,
|
|
2777
2840
|
messageId: message.messageId,
|
|
2841
|
+
baseagent: normalizedBaseagent.canonical,
|
|
2778
2842
|
channel: configChannelType,
|
|
2779
2843
|
channelId: message.channelId,
|
|
2780
2844
|
chatType: configChatType,
|
|
@@ -3833,6 +3897,7 @@ export class ResponseEngine {
|
|
|
3833
3897
|
if (this.activeRenderers.get(session.id)?.taskId === taskId) {
|
|
3834
3898
|
this.activeRenderers.delete(session.id);
|
|
3835
3899
|
}
|
|
3900
|
+
this.clearPauseSession(session.id, taskPauseController);
|
|
3836
3901
|
}
|
|
3837
3902
|
// [迁移探针] 任务收尾:记录工具提醒最终状态并落盘(防线 1)
|
|
3838
3903
|
if (snapshot.isEnabled()) {
|
|
@@ -4419,8 +4484,15 @@ export class ResponseEngine {
|
|
|
4419
4484
|
// 重置最后回复追踪
|
|
4420
4485
|
lastReplyText = '';
|
|
4421
4486
|
hasProjectedCurrentReplyText = false;
|
|
4487
|
+
const lifecycleEventKey = buildToolLifecycleEventKey({
|
|
4488
|
+
sessionId: session.id,
|
|
4489
|
+
callId: event.callId,
|
|
4490
|
+
correlationId: event.correlationId,
|
|
4491
|
+
});
|
|
4422
4492
|
this.eventBus.publish({
|
|
4423
4493
|
type: 'tool:use',
|
|
4494
|
+
eventKey: lifecycleEventKey,
|
|
4495
|
+
eventPhase: 'use',
|
|
4424
4496
|
sessionId: session.id,
|
|
4425
4497
|
toolName: event.name,
|
|
4426
4498
|
input: event.input,
|
|
@@ -4428,6 +4500,10 @@ export class ResponseEngine {
|
|
|
4428
4500
|
...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
|
|
4429
4501
|
agentAid: session.selfAID ?? 'unknown',
|
|
4430
4502
|
permissionMode: permissionMode ?? 'unknown',
|
|
4503
|
+
decision: 'pending',
|
|
4504
|
+
decisionSource: 'runner',
|
|
4505
|
+
executed: false,
|
|
4506
|
+
executionState: 'requested',
|
|
4431
4507
|
timestamp: Date.now(),
|
|
4432
4508
|
causation,
|
|
4433
4509
|
});
|
|
@@ -4486,8 +4562,15 @@ export class ResponseEngine {
|
|
|
4486
4562
|
logger.warn('[ResponseEngine] auto clear queue after ec ctl queue failed:', error);
|
|
4487
4563
|
}
|
|
4488
4564
|
}
|
|
4565
|
+
const lifecycleEventKey = buildToolLifecycleEventKey({
|
|
4566
|
+
sessionId: session.id,
|
|
4567
|
+
callId: event.callId,
|
|
4568
|
+
correlationId: event.correlationId,
|
|
4569
|
+
});
|
|
4489
4570
|
this.eventBus.publish({
|
|
4490
4571
|
type: 'tool:result',
|
|
4572
|
+
eventKey: lifecycleEventKey,
|
|
4573
|
+
eventPhase: 'result',
|
|
4491
4574
|
sessionId: session.id,
|
|
4492
4575
|
toolName: event.name,
|
|
4493
4576
|
isError: event.isError,
|
|
@@ -4497,6 +4580,10 @@ export class ResponseEngine {
|
|
|
4497
4580
|
...(event.correlationId || event.callId ? { correlationId: event.correlationId ?? event.callId } : {}),
|
|
4498
4581
|
agentAid: session.selfAID ?? 'unknown',
|
|
4499
4582
|
permissionMode: permissionMode ?? 'unknown',
|
|
4583
|
+
decision: event.isError ? 'error' : 'allow',
|
|
4584
|
+
decisionSource: 'runner',
|
|
4585
|
+
executed: true,
|
|
4586
|
+
executionState: event.isError ? 'failed' : 'completed',
|
|
4500
4587
|
timestamp: Date.now(),
|
|
4501
4588
|
causation,
|
|
4502
4589
|
});
|