evolcore 0.0.9 → 0.0.11
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 +29 -0
- package/README.md +3 -3
- package/dist/agents/baseagent.js +4 -0
- package/dist/agents/claude-runner.js +123 -42
- package/dist/agents/codex-app-server-client.js +33 -9
- package/dist/agents/codex-runner.js +58 -8
- package/dist/agents/ecagent-runner.js +17 -2
- package/dist/agents/request-identity.js +55 -0
- package/dist/aun/outbox.js +28 -31
- package/dist/channels/aun.js +131 -128
- package/dist/cli/agent-command.js +16 -9
- package/dist/cli/agent.js +82 -19
- package/dist/cli/daemon-commands.js +21 -2
- package/dist/cli/index.js +76 -61
- package/dist/cli/init-cancel.js +208 -0
- package/dist/cli/init-channel.js +343 -195
- package/dist/cli/init.js +21 -9
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/contact-book-store.js +1 -1
- package/dist/config/gateway-config.js +26 -10
- package/dist/core/agent-reload-coordinator.js +53 -0
- package/dist/core/auth/operation-authorizer.js +32 -147
- package/dist/core/auth/operation-catalog.js +80 -0
- package/dist/core/bootstrap-messages.js +50 -0
- package/dist/core/bootstrap-service.js +85 -10
- package/dist/core/channel-loader.js +23 -6
- package/dist/core/command/agent-control.js +14 -11
- package/dist/core/command/menu-handler.js +67 -76
- package/dist/core/command/slash-handler.js +4 -4
- package/dist/core/data-migration.js +79 -27
- package/dist/core/evolagent-registry.js +125 -35
- package/dist/core/evolagent.js +8 -3
- package/dist/core/inference/text-inference.js +38 -4
- package/dist/core/message/message-bridge.js +1 -1
- package/dist/core/message/message-log.js +22 -0
- package/dist/core/message/message-queue.js +19 -4
- package/dist/core/model/model-catalog.js +143 -24
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/index.js +1 -0
- package/dist/core/permission/readonly-shell-query.js +532 -0
- package/dist/core/permission/shell-environment.js +46 -0
- package/dist/core/permission/tool-policy.js +231 -93
- package/dist/core/protected-paths.js +10 -7
- package/dist/core/runner-reload-transaction.js +57 -0
- package/dist/index.js +262 -84
- package/dist/ipc.js +29 -11
- package/dist/utils/aid-bind.js +3 -8
- package/dist/utils/log-writer.js +6 -10
- package/dist/utils/logger.js +5 -5
- package/kits/docs/evolcore/msg.md +13 -0
- package/kits/rules/01-overview.md +9 -0
- package/kits/schemas/agent-config.schema.3.json +1 -1
- package/kits/schemas/agent-config.schema.4.json +1 -1
- package/kits/schemas/relation-config.schema.2.json +1 -1
- package/kits/schemas/role-config.schema.1.json +1 -1
- package/kits/templates/roles/admin.json +5 -0
- package/kits/templates/roles/member.json +17 -0
- package/kits/templates/roles/visitor.json +8 -0
- package/kits/templates/system-fragments/bootstrap.md +12 -6
- package/kits/templates/system-fragments/channel.md +6 -0
- package/kits/templates/system-fragments/session.md +2 -0
- package/package.json +2 -1
- package/skills/eclink/SKILL.md +15 -3
- package/skills/eclink/agents/openai.yaml +3 -3
|
@@ -54,6 +54,14 @@ function operationTopologyKey(operation) {
|
|
|
54
54
|
sessionChannelKey: operation.sessionChannelKey,
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
+
function operationProofTopologyKey(operation) {
|
|
58
|
+
if (operation.kind !== 'contact-audit-partition')
|
|
59
|
+
return operationTopologyKey(operation);
|
|
60
|
+
return JSON.stringify({
|
|
61
|
+
kind: operation.kind,
|
|
62
|
+
source: operation.source,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
57
65
|
/**
|
|
58
66
|
* Manifests written before allowExistingDestinationMerge was introduced still
|
|
59
67
|
* need safe recovery. Re-derive that narrow permission from their immutable
|
|
@@ -125,26 +133,40 @@ function acknowledgeCompletedCopies(root, operations) {
|
|
|
125
133
|
if (manifest.planSignature === currentPlanSignature)
|
|
126
134
|
return;
|
|
127
135
|
for (const operation of manifest.operations ?? []) {
|
|
128
|
-
if (operation.kind !== 'copy'
|
|
136
|
+
if ((operation.kind !== 'copy' && operation.kind !== 'contact-audit-partition')
|
|
137
|
+
|| (operation.status !== 'committed' && operation.status !== 'skipped')
|
|
138
|
+
|| !operation.sourceHash)
|
|
129
139
|
continue;
|
|
130
|
-
const key =
|
|
131
|
-
if (!proofs.has(key))
|
|
132
|
-
proofs.set(key, {
|
|
140
|
+
const key = operationProofTopologyKey(operation);
|
|
141
|
+
if (!proofs.has(key)) {
|
|
142
|
+
proofs.set(key, {
|
|
143
|
+
migrationId: manifest.id,
|
|
144
|
+
sourceHash: operation.sourceHash,
|
|
145
|
+
destinations: operation.kind === 'contact-audit-partition'
|
|
146
|
+
? [...(operation.destinations ?? [])]
|
|
147
|
+
: operation.destination ? [operation.destination] : [],
|
|
148
|
+
});
|
|
149
|
+
}
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
}
|
|
136
153
|
catch { }
|
|
137
154
|
for (const operation of operations) {
|
|
138
|
-
if (operation.kind !== 'copy'
|
|
155
|
+
if (operation.kind !== 'copy' && operation.kind !== 'contact-audit-partition')
|
|
139
156
|
continue;
|
|
140
|
-
const proof = proofs.get(
|
|
157
|
+
const proof = proofs.get(operationProofTopologyKey(operation));
|
|
141
158
|
if (!proof)
|
|
142
159
|
continue;
|
|
160
|
+
if (operation.kind === 'copy' && (!operation.destination || !fs.existsSync(operation.destination)))
|
|
161
|
+
continue;
|
|
162
|
+
if (operation.kind === 'contact-audit-partition' && proof.destinations.some(destination => !fs.existsSync(destination)))
|
|
163
|
+
continue;
|
|
143
164
|
try {
|
|
144
165
|
if (treeHash(operation.source, operation.exclude) !== proof.sourceHash)
|
|
145
166
|
continue;
|
|
146
167
|
operation.status = 'acknowledged';
|
|
147
168
|
operation.acknowledgedBy = proof.migrationId;
|
|
169
|
+
operation.sourceHash = proof.sourceHash;
|
|
148
170
|
}
|
|
149
171
|
catch { }
|
|
150
172
|
}
|
|
@@ -1419,6 +1441,18 @@ function atomicWriteText(target, content) {
|
|
|
1419
1441
|
function hashContent(content) {
|
|
1420
1442
|
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
1421
1443
|
}
|
|
1444
|
+
function mergeJsonlContent(existing, incoming, deduplicateExisting = false) {
|
|
1445
|
+
const existingLines = existing.split('\n').filter(line => line.trim());
|
|
1446
|
+
const merged = deduplicateExisting ? [...new Set(existingLines)] : [...existingLines];
|
|
1447
|
+
const lines = new Set(merged);
|
|
1448
|
+
for (const line of incoming.split('\n')) {
|
|
1449
|
+
if (!line.trim() || lines.has(line))
|
|
1450
|
+
continue;
|
|
1451
|
+
lines.add(line);
|
|
1452
|
+
merged.push(line);
|
|
1453
|
+
}
|
|
1454
|
+
return merged.length ? `${merged.join('\n')}\n` : '';
|
|
1455
|
+
}
|
|
1422
1456
|
function appendIssue(operation, issue) {
|
|
1423
1457
|
const issues = operation.issues ?? [];
|
|
1424
1458
|
if (!issues.includes(issue))
|
|
@@ -1457,20 +1491,18 @@ function executeContactAudit(manifest, operation, checkpoint) {
|
|
|
1457
1491
|
for (const [aid, lines] of groups) {
|
|
1458
1492
|
const destination = path.join(manifest.root, 'agents', aid, 'data', 'contact-audit.jsonl');
|
|
1459
1493
|
const content = lines.join('\n') + '\n';
|
|
1460
|
-
const
|
|
1494
|
+
const existing = fs.existsSync(destination) ? fs.readFileSync(destination, 'utf8') : undefined;
|
|
1495
|
+
if (existing !== undefined)
|
|
1496
|
+
strictJsonValidation(destination);
|
|
1497
|
+
const expectedContent = existing === undefined ? content : mergeJsonlContent(existing, content);
|
|
1498
|
+
const expectedHash = hashContent(expectedContent);
|
|
1461
1499
|
operation.destinations.push(destination);
|
|
1462
1500
|
operation.destinationHashes[destination] = expectedHash;
|
|
1463
|
-
if (
|
|
1464
|
-
const existing = fs.readFileSync(destination, 'utf8');
|
|
1501
|
+
if (existing !== undefined) {
|
|
1465
1502
|
if (hashContent(existing) !== expectedHash) {
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
1468
|
-
|
|
1469
|
-
if (!content.startsWith(existing)) {
|
|
1470
|
-
appendIssue(operation, `destination conflict: ${destination}`);
|
|
1471
|
-
continue;
|
|
1472
|
-
}
|
|
1473
|
-
atomicWriteText(destination, content);
|
|
1503
|
+
// The per-Agent audit may have advanced after an earlier migration.
|
|
1504
|
+
// Preserve its order and append only legacy records not already present.
|
|
1505
|
+
atomicWriteText(destination, expectedContent);
|
|
1474
1506
|
}
|
|
1475
1507
|
continue;
|
|
1476
1508
|
}
|
|
@@ -1600,15 +1632,7 @@ function trackMergeSources(operation) {
|
|
|
1600
1632
|
return sources;
|
|
1601
1633
|
}
|
|
1602
1634
|
function mergeJsonl(target, source) {
|
|
1603
|
-
|
|
1604
|
-
const merged = [...lines];
|
|
1605
|
-
for (const line of fs.readFileSync(source, 'utf8').split('\n')) {
|
|
1606
|
-
if (!line.trim() || lines.has(line))
|
|
1607
|
-
continue;
|
|
1608
|
-
lines.add(line);
|
|
1609
|
-
merged.push(line);
|
|
1610
|
-
}
|
|
1611
|
-
fs.writeFileSync(target, merged.length ? `${merged.join('\n')}\n` : '');
|
|
1635
|
+
fs.writeFileSync(target, mergeJsonlContent(fs.readFileSync(target, 'utf8'), fs.readFileSync(source, 'utf8'), true));
|
|
1612
1636
|
}
|
|
1613
1637
|
function copyTree(source, destination) {
|
|
1614
1638
|
const stat = fs.statSync(source);
|
|
@@ -1741,6 +1765,34 @@ export function readDataMigration(root, id) {
|
|
|
1741
1765
|
throw new Error(`migration manifest not found: ${id}`);
|
|
1742
1766
|
return manifest;
|
|
1743
1767
|
}
|
|
1768
|
+
function completedMigrationMatchesPlan(manifest, plan, planSignature) {
|
|
1769
|
+
if (manifest.state !== 'completed' || manifest.planSignature !== planSignature)
|
|
1770
|
+
return false;
|
|
1771
|
+
const proofs = new Map(manifest.operations.map(operation => [operationProofTopologyKey(operation), operation]));
|
|
1772
|
+
for (const operation of plan.operations.filter(item => item.status !== 'acknowledged')) {
|
|
1773
|
+
const proof = proofs.get(operationProofTopologyKey(operation));
|
|
1774
|
+
if (!proof || (proof.status !== 'committed' && proof.status !== 'skipped' && proof.status !== 'acknowledged'))
|
|
1775
|
+
return false;
|
|
1776
|
+
try {
|
|
1777
|
+
for (const source of operationSources(operation)) {
|
|
1778
|
+
const expected = proof.sourceHashes?.[source]
|
|
1779
|
+
?? (source === proof.source ? proof.sourceHash : undefined);
|
|
1780
|
+
if (!expected || treeHash(source, operation.exclude) !== expected)
|
|
1781
|
+
return false;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
catch {
|
|
1785
|
+
return false;
|
|
1786
|
+
}
|
|
1787
|
+
if ((operation.kind === 'copy' || operation.kind === 'merge-copy')
|
|
1788
|
+
&& (!operation.destination || !fs.existsSync(operation.destination)))
|
|
1789
|
+
return false;
|
|
1790
|
+
if (operation.kind === 'contact-audit-partition'
|
|
1791
|
+
&& (proof.destinations ?? []).some(destination => !fs.existsSync(destination)))
|
|
1792
|
+
return false;
|
|
1793
|
+
}
|
|
1794
|
+
return true;
|
|
1795
|
+
}
|
|
1744
1796
|
/**
|
|
1745
1797
|
* Startup gate for the explicit migration workflow. Legacy source files are
|
|
1746
1798
|
* deliberately retained after success, so their mere presence is not enough
|
|
@@ -1763,7 +1815,7 @@ export function inspectDataMigrationRequirement(root) {
|
|
|
1763
1815
|
if (!entry.isFile() || !entry.name.endsWith('.json'))
|
|
1764
1816
|
continue;
|
|
1765
1817
|
const manifest = readJson(path.join(migrationDir(root), entry.name));
|
|
1766
|
-
if (manifest
|
|
1818
|
+
if (!manifest || !completedMigrationMatchesPlan(manifest, plan, planSignature))
|
|
1767
1819
|
continue;
|
|
1768
1820
|
completedMigrationId = manifest.id;
|
|
1769
1821
|
break;
|
|
@@ -5,7 +5,7 @@ import { logger } from '../utils/logger.js';
|
|
|
5
5
|
import { agentPersonalDir } from '../paths.js';
|
|
6
6
|
import { invalidateAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
7
7
|
import { loadAllAgents, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
|
|
8
|
-
import { resolveEffective } from '../config/config-manager.js';
|
|
8
|
+
import { ConfigTarget, read as readConfig, resolveEffective } from '../config/config-manager.js';
|
|
9
9
|
// ── Channel Fingerprint ───────────────────────────────────────────────────
|
|
10
10
|
// 用于检测多 agent 之间复用同一外部凭证的冲突(appId、aid、token 等)。
|
|
11
11
|
// 格式:{type}:{primaryKey}
|
|
@@ -84,18 +84,20 @@ export class EvolAgentRegistry {
|
|
|
84
84
|
this.skipped = [];
|
|
85
85
|
const { agents: rawAgents, skipped, invalidAgents = [] } = loadAllAgents({ includeInvalid: true });
|
|
86
86
|
this.skipped = skipped;
|
|
87
|
-
for (const
|
|
87
|
+
for (const expandedRaw of rawAgents) {
|
|
88
88
|
try {
|
|
89
|
-
const
|
|
89
|
+
const raw = readConfig(ConfigTarget.Agent, { self: expandedRaw.aid }, { cache: true }) ?? expandedRaw;
|
|
90
|
+
const merged = resolveEffective({ self: expandedRaw.aid }, { expand: true });
|
|
90
91
|
const agent = new EvolAgent(raw, merged);
|
|
91
|
-
ensureAgentDirSkeleton(
|
|
92
|
+
ensureAgentDirSkeleton(expandedRaw.aid);
|
|
92
93
|
this.agents.set(agent.aid, agent);
|
|
93
94
|
}
|
|
94
95
|
catch (e) {
|
|
95
|
-
logger.warn(`[EvolAgentRegistry] failed to construct agent ${
|
|
96
|
+
logger.warn(`[EvolAgentRegistry] failed to construct agent ${expandedRaw.aid}: ${e}`);
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
|
-
for (const { agent:
|
|
99
|
+
for (const { agent: expandedRaw, reason } of invalidAgents) {
|
|
100
|
+
const raw = readConfig(ConfigTarget.Agent, { self: expandedRaw.aid }, { cache: true }) ?? expandedRaw;
|
|
99
101
|
this.registerErrorAgent(raw, reason);
|
|
100
102
|
}
|
|
101
103
|
this.detectAndFlagConflicts();
|
|
@@ -119,7 +121,7 @@ export class EvolAgentRegistry {
|
|
|
119
121
|
}
|
|
120
122
|
resolveMergedForErrorAgent(raw, reason) {
|
|
121
123
|
try {
|
|
122
|
-
return resolveEffective({ self: raw.aid });
|
|
124
|
+
return resolveEffective({ self: raw.aid }, { expand: true });
|
|
123
125
|
}
|
|
124
126
|
catch (e) {
|
|
125
127
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -225,9 +227,9 @@ export class EvolAgentRegistry {
|
|
|
225
227
|
logger.info(`[EvolAgentRegistry] agent ${aid} already loaded, skipping`);
|
|
226
228
|
return this.agents.get(aid);
|
|
227
229
|
}
|
|
228
|
-
let
|
|
230
|
+
let expandedRaw = null;
|
|
229
231
|
try {
|
|
230
|
-
|
|
232
|
+
expandedRaw = loadAgent(aid);
|
|
231
233
|
}
|
|
232
234
|
catch (e) {
|
|
233
235
|
const reason = e instanceof Error ? e.message : String(e);
|
|
@@ -239,22 +241,23 @@ export class EvolAgentRegistry {
|
|
|
239
241
|
channels: [],
|
|
240
242
|
}, reason);
|
|
241
243
|
}
|
|
242
|
-
if (!
|
|
244
|
+
if (!expandedRaw) {
|
|
243
245
|
logger.warn(`[EvolAgentRegistry] loadNewAgent: ${aid}/config.json not found`);
|
|
244
246
|
return null;
|
|
245
247
|
}
|
|
246
|
-
const errs = validateAgentConfig(
|
|
248
|
+
const errs = validateAgentConfig(expandedRaw);
|
|
247
249
|
if (errs.length > 0) {
|
|
248
250
|
const reason = errs.join('; ');
|
|
249
251
|
logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${reason}`);
|
|
250
|
-
return this.registerErrorAgent(
|
|
252
|
+
return this.registerErrorAgent(expandedRaw, reason);
|
|
251
253
|
}
|
|
252
|
-
const conflict = this.checkConflictForReload(
|
|
254
|
+
const conflict = this.checkConflictForReload(expandedRaw, aid);
|
|
253
255
|
if (conflict) {
|
|
254
256
|
logger.warn(`[EvolAgentRegistry] loadNewAgent ${aid}: ${conflict}`);
|
|
255
|
-
return this.registerErrorAgent(
|
|
257
|
+
return this.registerErrorAgent(expandedRaw, `Channel conflict: ${conflict}`);
|
|
256
258
|
}
|
|
257
|
-
const
|
|
259
|
+
const raw = readConfig(ConfigTarget.Agent, { self: aid }, { cache: true }) ?? expandedRaw;
|
|
260
|
+
const merged = resolveEffective({ self: aid }, { expand: true });
|
|
258
261
|
const agent = new EvolAgent(raw, merged);
|
|
259
262
|
ensureAgentDirSkeleton(aid);
|
|
260
263
|
this.agents.set(aid, agent);
|
|
@@ -268,13 +271,14 @@ export class EvolAgentRegistry {
|
|
|
268
271
|
const oldAgent = this.agents.get(aidOrName);
|
|
269
272
|
if (!oldAgent)
|
|
270
273
|
throw new Error(`Agent "${aidOrName}" not found`);
|
|
271
|
-
const
|
|
272
|
-
if (!
|
|
274
|
+
const expandedRaw = loadAgent(oldAgent.aid);
|
|
275
|
+
if (!expandedRaw)
|
|
273
276
|
throw new Error(`Agent ${oldAgent.aid}/config.json missing on reload`);
|
|
274
|
-
const errs = validateAgentConfig(
|
|
277
|
+
const errs = validateAgentConfig(expandedRaw);
|
|
275
278
|
if (errs.length > 0)
|
|
276
279
|
throw new Error(`Invalid config after edit: ${errs.join('; ')}`);
|
|
277
|
-
const
|
|
280
|
+
const raw = readConfig(ConfigTarget.Agent, { self: oldAgent.aid }, { cache: true }) ?? expandedRaw;
|
|
281
|
+
const merged = resolveEffective({ self: raw.aid }, { expand: true });
|
|
278
282
|
if (oldAgent.status === 'disabled' && raw.enabled !== false) {
|
|
279
283
|
oldAgent.swapConfig(raw, merged);
|
|
280
284
|
const hotLoad = globalThis.__evolcore_hotLoadAgent;
|
|
@@ -289,32 +293,69 @@ export class EvolAgentRegistry {
|
|
|
289
293
|
}
|
|
290
294
|
if (oldAgent.status !== 'disabled' && raw.enabled === false) {
|
|
291
295
|
let prepared = false;
|
|
296
|
+
let runnerTransaction;
|
|
297
|
+
const disconnectedChannels = [];
|
|
298
|
+
const previousConfig = oldAgent.captureConfigSnapshot();
|
|
299
|
+
const previousStatus = oldAgent.status;
|
|
300
|
+
const previousError = oldAgent.error;
|
|
292
301
|
try {
|
|
293
302
|
await hooks.prepareHandoffReload?.(oldAgent.aid);
|
|
294
303
|
prepared = true;
|
|
304
|
+
runnerTransaction = await hooks.stageAgentRunners?.(new EvolAgent(raw, merged));
|
|
295
305
|
for (const ch of oldAgent.channelInstanceNames()) {
|
|
296
306
|
try {
|
|
297
307
|
await hooks.drainChannel(ch);
|
|
298
308
|
}
|
|
299
309
|
catch { }
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
catch { }
|
|
310
|
+
await hooks.disconnectChannel(ch);
|
|
311
|
+
disconnectedChannels.push(ch);
|
|
304
312
|
}
|
|
305
313
|
oldAgent.swapConfig(raw, merged);
|
|
306
314
|
oldAgent.status = 'disabled';
|
|
307
315
|
this.channelIndex.clear();
|
|
308
316
|
this.buildChannelIndex();
|
|
317
|
+
runnerTransaction?.commit();
|
|
318
|
+
try {
|
|
319
|
+
await runnerTransaction?.finalize();
|
|
320
|
+
}
|
|
321
|
+
catch (error) {
|
|
322
|
+
logger.warn(`[Reload] Failed to dispose previous runners after disabling "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
323
|
+
}
|
|
324
|
+
prepared = false;
|
|
309
325
|
logger.info(`[Reload] Agent "${aidOrName}" disabled`);
|
|
310
326
|
return;
|
|
311
327
|
}
|
|
312
328
|
catch (error) {
|
|
329
|
+
const rollbackErrors = [];
|
|
330
|
+
oldAgent.swapConfig(previousConfig.rawAgent, previousConfig.merged);
|
|
331
|
+
oldAgent.invalidatePersonaCache();
|
|
332
|
+
oldAgent.status = previousStatus;
|
|
333
|
+
oldAgent.error = previousError;
|
|
334
|
+
await runnerTransaction?.rollback().catch(rollbackError => {
|
|
335
|
+
rollbackErrors.push(`runners: ${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`);
|
|
336
|
+
});
|
|
337
|
+
for (const ch of disconnectedChannels) {
|
|
338
|
+
try {
|
|
339
|
+
await hooks.startChannel(oldAgent, ch);
|
|
340
|
+
}
|
|
341
|
+
catch (restartError) {
|
|
342
|
+
rollbackErrors.push(`restart ${ch}: ${restartError instanceof Error ? restartError.message : String(restartError)}`);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
this.channelIndex.clear();
|
|
346
|
+
this.buildChannelIndex();
|
|
313
347
|
if (prepared) {
|
|
314
348
|
try {
|
|
315
349
|
await hooks.completeHandoffReload?.(oldAgent.aid);
|
|
316
350
|
}
|
|
317
|
-
catch {
|
|
351
|
+
catch (resumeError) {
|
|
352
|
+
rollbackErrors.push(`handoff: ${resumeError instanceof Error ? resumeError.message : String(resumeError)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (rollbackErrors.length > 0) {
|
|
356
|
+
oldAgent.status = 'error';
|
|
357
|
+
oldAgent.error = `Disable failed; rollback incomplete: ${rollbackErrors.join('; ')}`;
|
|
358
|
+
logger.error(`[Reload] Disable rollback incomplete for "${aidOrName}": ${rollbackErrors.join('; ')}`);
|
|
318
359
|
}
|
|
319
360
|
throw error;
|
|
320
361
|
}
|
|
@@ -324,13 +365,18 @@ export class EvolAgentRegistry {
|
|
|
324
365
|
throw new Error(`Channel conflict: ${conflict}`);
|
|
325
366
|
const removedSuccessfully = [];
|
|
326
367
|
const addedSuccessfully = [];
|
|
368
|
+
let runnerTransaction;
|
|
327
369
|
let prepared = false;
|
|
370
|
+
const previousConfig = oldAgent.captureConfigSnapshot();
|
|
371
|
+
const previousStatus = oldAgent.status;
|
|
372
|
+
const previousError = oldAgent.error;
|
|
328
373
|
try {
|
|
329
374
|
await hooks.prepareHandoffReload?.(oldAgent.aid);
|
|
330
375
|
prepared = true;
|
|
376
|
+
runnerTransaction = await hooks.stageAgentRunners?.(new EvolAgent(raw, merged));
|
|
331
377
|
const oldChannels = new Set(oldAgent.channelInstanceNames());
|
|
332
378
|
const aunKey = oldAgent.effectiveChannelName('aun', 'main');
|
|
333
|
-
const otherKeys =
|
|
379
|
+
const otherKeys = merged.channels.filter(c => c.type !== 'aun').map(c => oldAgent.effectiveChannelName(c.type, c.name));
|
|
334
380
|
const newChannels = new Set([aunKey, ...otherKeys]);
|
|
335
381
|
const toRemove = [...oldChannels].filter(c => !newChannels.has(c));
|
|
336
382
|
const toAdd = [...newChannels].filter(c => !oldChannels.has(c));
|
|
@@ -338,7 +384,7 @@ export class EvolAgentRegistry {
|
|
|
338
384
|
const credentialsChanged = [];
|
|
339
385
|
for (const ch of kept) {
|
|
340
386
|
const oldInst = oldAgent.findChannelInstance(ch);
|
|
341
|
-
const newInst = findInstanceByKey(
|
|
387
|
+
const newInst = findInstanceByKey(merged, oldAgent, ch);
|
|
342
388
|
if (oldInst && newInst && JSON.stringify(oldInst) !== JSON.stringify(newInst)) {
|
|
343
389
|
credentialsChanged.push(ch);
|
|
344
390
|
}
|
|
@@ -360,28 +406,72 @@ export class EvolAgentRegistry {
|
|
|
360
406
|
oldAgent.status = 'running';
|
|
361
407
|
this.channelIndex.clear();
|
|
362
408
|
this.buildChannelIndex();
|
|
363
|
-
|
|
364
|
-
|
|
409
|
+
runnerTransaction?.commit();
|
|
410
|
+
try {
|
|
411
|
+
await hooks.completeHandoffReload?.(oldAgent.aid);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
logger.warn(`[Reload] Handoff recovery failed after commit for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
prepared = false;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
await hooks.afterReload?.(oldAgent);
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
logger.warn(`[Reload] Post-reload hook failed after commit for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
424
|
+
}
|
|
425
|
+
try {
|
|
426
|
+
await runnerTransaction?.finalize();
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
logger.warn(`[Reload] Failed to dispose previous runners for "${aidOrName}": ${error instanceof Error ? error.message : String(error)}`);
|
|
430
|
+
}
|
|
365
431
|
}
|
|
366
432
|
catch (err) {
|
|
367
|
-
logger.error(`[Reload] Failed: ${err}. Attempting rollback for "${aidOrName}".`);
|
|
368
|
-
|
|
433
|
+
logger.error(`[Reload] Failed before commit: ${err}. Attempting rollback for "${aidOrName}".`);
|
|
434
|
+
const rollbackErrors = [];
|
|
435
|
+
for (const ch of [...addedSuccessfully].reverse()) {
|
|
369
436
|
try {
|
|
370
437
|
await hooks.disconnectChannel(ch);
|
|
371
438
|
}
|
|
372
|
-
catch {
|
|
439
|
+
catch (error) {
|
|
440
|
+
rollbackErrors.push(`disconnect ${ch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
441
|
+
}
|
|
373
442
|
}
|
|
443
|
+
oldAgent.swapConfig(previousConfig.rawAgent, previousConfig.merged);
|
|
444
|
+
oldAgent.invalidatePersonaCache();
|
|
445
|
+
await runnerTransaction?.rollback().catch(error => {
|
|
446
|
+
rollbackErrors.push(`runners: ${error instanceof Error ? error.message : String(error)}`);
|
|
447
|
+
});
|
|
448
|
+
for (const ch of [...new Set(removedSuccessfully)]) {
|
|
449
|
+
try {
|
|
450
|
+
await hooks.startChannel(oldAgent, ch);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
rollbackErrors.push(`restart ${ch}: ${error instanceof Error ? error.message : String(error)}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
this.channelIndex.clear();
|
|
457
|
+
this.buildChannelIndex();
|
|
374
458
|
if (prepared) {
|
|
375
459
|
try {
|
|
376
460
|
await hooks.completeHandoffReload?.(oldAgent.aid);
|
|
377
461
|
}
|
|
378
462
|
catch (resumeError) {
|
|
379
|
-
|
|
463
|
+
rollbackErrors.push(`handoff: ${resumeError instanceof Error ? resumeError.message : String(resumeError)}`);
|
|
380
464
|
}
|
|
381
465
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
466
|
+
if (rollbackErrors.length === 0) {
|
|
467
|
+
oldAgent.status = previousStatus;
|
|
468
|
+
oldAgent.error = previousError;
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
oldAgent.status = 'error';
|
|
472
|
+
oldAgent.error = `Reload failed; rollback incomplete: ${rollbackErrors.join('; ')}`;
|
|
473
|
+
logger.error(`[Reload] Rollback incomplete for "${aidOrName}": ${rollbackErrors.join('; ')}`);
|
|
474
|
+
}
|
|
385
475
|
throw err;
|
|
386
476
|
}
|
|
387
477
|
}
|
package/dist/core/evolagent.js
CHANGED
|
@@ -52,7 +52,7 @@ export class EvolAgent {
|
|
|
52
52
|
const ba = this.baseagent;
|
|
53
53
|
// 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
|
|
54
54
|
try {
|
|
55
|
-
const effective = resolveEffective({ self: this.aid }, { cache: true });
|
|
55
|
+
const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
|
|
56
56
|
const block = effective.baseagents?.[ba];
|
|
57
57
|
return block?.model;
|
|
58
58
|
}
|
|
@@ -66,7 +66,7 @@ export class EvolAgent {
|
|
|
66
66
|
const ba = this.baseagent;
|
|
67
67
|
// 动态读取配置(基于 fileCache + mtime),使配置变更立即体现在显示中
|
|
68
68
|
try {
|
|
69
|
-
const effective = resolveEffective({ self: this.aid }, { cache: true });
|
|
69
|
+
const effective = resolveEffective({ self: this.aid }, { cache: true, expand: true });
|
|
70
70
|
const block = effective.baseagents?.[ba];
|
|
71
71
|
if (ba === 'codex')
|
|
72
72
|
return block?.effort ?? block?.reasoning;
|
|
@@ -257,7 +257,8 @@ export class EvolAgent {
|
|
|
257
257
|
this.persist();
|
|
258
258
|
}
|
|
259
259
|
setLifecycle(value) {
|
|
260
|
-
|
|
260
|
+
const current = cfgRead(ConfigTarget.Agent, { self: this.aid });
|
|
261
|
+
this.rawAgent = withLifecycleForWrite(current || this.rawAgent, value);
|
|
261
262
|
this.rawAgent.$schema_version = Math.max(this.rawAgent.$schema_version || 0, this.merged.$schema_version || 0);
|
|
262
263
|
this.merged.lifecycle = value;
|
|
263
264
|
delete this.merged.initialized;
|
|
@@ -318,6 +319,10 @@ export class EvolAgent {
|
|
|
318
319
|
this.rawAgent = rawAgent;
|
|
319
320
|
this.merged = merged;
|
|
320
321
|
}
|
|
322
|
+
/** Reload uses the current object references as an in-memory rollback snapshot. */
|
|
323
|
+
captureConfigSnapshot() {
|
|
324
|
+
return { rawAgent: this.rawAgent, merged: this.merged };
|
|
325
|
+
}
|
|
321
326
|
// ── 内部辅助 ─────────────────────────────────────────────────────────
|
|
322
327
|
/**
|
|
323
328
|
* 找 rawAgent.channels 里的可变实例,用于写入。
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveEcagentConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
|
|
2
|
+
import { buildModelRequestHeaders } from '../../agents/request-identity.js';
|
|
2
3
|
function apiEndpoint(baseUrl, resource) {
|
|
3
4
|
const configured = baseUrl?.trim();
|
|
4
5
|
if (!configured)
|
|
@@ -119,12 +120,25 @@ export class EcagentTextInferenceProvider {
|
|
|
119
120
|
baseagent = 'ecagent';
|
|
120
121
|
constructor(config) {
|
|
121
122
|
this.config = config;
|
|
123
|
+
this.config = {
|
|
124
|
+
...config,
|
|
125
|
+
headers: buildModelRequestHeaders({
|
|
126
|
+
baseagent: 'ecagent',
|
|
127
|
+
baseUrl: config.baseUrl,
|
|
128
|
+
agentAid: config.evolcoreAgentAid,
|
|
129
|
+
configuredHeaders: config.headers,
|
|
130
|
+
}),
|
|
131
|
+
};
|
|
122
132
|
}
|
|
123
133
|
async completeText(request) {
|
|
124
134
|
const response = await fetch(apiEndpoint(this.config.baseUrl, 'chat/completions'), {
|
|
125
135
|
method: 'POST',
|
|
126
136
|
signal: request.signal,
|
|
127
|
-
headers: {
|
|
137
|
+
headers: {
|
|
138
|
+
'Content-Type': 'application/json',
|
|
139
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
140
|
+
...this.config.headers,
|
|
141
|
+
},
|
|
128
142
|
body: JSON.stringify({
|
|
129
143
|
model: request.model,
|
|
130
144
|
messages: [{ role: 'system', content: request.system }, { role: 'user', content: request.input }],
|
|
@@ -150,15 +164,35 @@ export function createTextInferenceProvider(baseagent, config) {
|
|
|
150
164
|
const canonical = normalizeBaseagent(baseagent).canonical;
|
|
151
165
|
if (canonical === 'claude') {
|
|
152
166
|
const resolved = resolveAnthropicDirectConfig(config.baseagents?.claude);
|
|
153
|
-
return resolved ? new AnthropicTextInferenceProvider(
|
|
167
|
+
return resolved ? new AnthropicTextInferenceProvider({
|
|
168
|
+
...resolved,
|
|
169
|
+
headers: buildModelRequestHeaders({
|
|
170
|
+
baseagent: 'claude',
|
|
171
|
+
baseUrl: resolved.baseUrl,
|
|
172
|
+
agentAid: config.aid,
|
|
173
|
+
configuredHeaders: resolved.headers,
|
|
174
|
+
}),
|
|
175
|
+
}) : undefined;
|
|
154
176
|
}
|
|
155
177
|
if (canonical === 'codex') {
|
|
156
178
|
const resolved = resolveOpenaiDirectConfig(config.baseagents?.codex);
|
|
157
|
-
return resolved ? new OpenAITextInferenceProvider(
|
|
179
|
+
return resolved ? new OpenAITextInferenceProvider({
|
|
180
|
+
...resolved,
|
|
181
|
+
headers: buildModelRequestHeaders({
|
|
182
|
+
baseagent: 'codex',
|
|
183
|
+
baseUrl: resolved.baseUrl,
|
|
184
|
+
agentAid: config.aid,
|
|
185
|
+
configuredHeaders: resolved.headers,
|
|
186
|
+
}),
|
|
187
|
+
}) : undefined;
|
|
158
188
|
}
|
|
159
189
|
if (canonical === 'ecagent') {
|
|
160
190
|
try {
|
|
161
|
-
const
|
|
191
|
+
const override = {
|
|
192
|
+
...(config.baseagents?.ecagent ?? {}),
|
|
193
|
+
evolcoreAgentAid: config.aid,
|
|
194
|
+
};
|
|
195
|
+
const resolved = resolveEcagentConfig({ agents: { ecagent: override } }, override);
|
|
162
196
|
return new EcagentTextInferenceProvider(resolved);
|
|
163
197
|
}
|
|
164
198
|
catch {
|
|
@@ -760,7 +760,7 @@ export class MessageBridge {
|
|
|
760
760
|
channelKey,
|
|
761
761
|
channelType: msg.channelType || effectiveChannelType,
|
|
762
762
|
channelId: msg.channelId,
|
|
763
|
-
recipientId: actorId,
|
|
763
|
+
recipientId: roleDetail.actor.principalId || actorId,
|
|
764
764
|
recipientName: msg.peerName,
|
|
765
765
|
source: 'inbound',
|
|
766
766
|
});
|
|
@@ -229,6 +229,27 @@ function formatTimestampMs(epochMs) {
|
|
|
229
229
|
export function messageLogPath(chatDir) {
|
|
230
230
|
return path.join(chatDir, MESSAGE_LOG_FILE);
|
|
231
231
|
}
|
|
232
|
+
export function hasMessageLogOperation(chatDir, operationId) {
|
|
233
|
+
const file = messageLogPath(chatDir);
|
|
234
|
+
if (!fs.existsSync(file))
|
|
235
|
+
return false;
|
|
236
|
+
try {
|
|
237
|
+
return fs.readFileSync(file, 'utf-8').split('\n').some(line => {
|
|
238
|
+
if (!line.trim())
|
|
239
|
+
return false;
|
|
240
|
+
try {
|
|
241
|
+
const entry = JSON.parse(line);
|
|
242
|
+
return entry?.dir === 'out' && entry.operationId === operationId;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
232
253
|
export function resolveChatDir(sessionsDir, channelType, channelId, selfAID) {
|
|
233
254
|
return chatDirPath(sessionsDir, channelType, channelId, selfAID);
|
|
234
255
|
}
|
|
@@ -323,6 +344,7 @@ export function buildOutboundEntry(opts) {
|
|
|
323
344
|
transport: opts.transport,
|
|
324
345
|
peerType: opts.peerType,
|
|
325
346
|
source: opts.source ?? 'daemon',
|
|
347
|
+
operationId: opts.operationId,
|
|
326
348
|
handoff_trace: opts.handoff_trace,
|
|
327
349
|
};
|
|
328
350
|
}
|
|
@@ -1361,9 +1361,9 @@ export class MessageQueue {
|
|
|
1361
1361
|
isAgentMuted(agentName) {
|
|
1362
1362
|
return this.mutedAgents.has(agentName);
|
|
1363
1363
|
}
|
|
1364
|
-
|
|
1365
|
-
interruptByAgent(agentName) {
|
|
1364
|
+
beginAgentInterrupt(agentName) {
|
|
1366
1365
|
let interrupted = 0;
|
|
1366
|
+
const barriers = [];
|
|
1367
1367
|
for (const [queueKey, name] of this.processingAgent) {
|
|
1368
1368
|
if ((name || DEFAULT_AGENT_NAME) === agentName) {
|
|
1369
1369
|
interrupted++;
|
|
@@ -1378,11 +1378,26 @@ export class MessageQueue {
|
|
|
1378
1378
|
causation: this.activeBatches.get(queueKey)?.message.causation,
|
|
1379
1379
|
});
|
|
1380
1380
|
if (this.interruptCallback) {
|
|
1381
|
-
|
|
1381
|
+
barriers.push(this.triggerInterrupt(queueKey, sessionKey, activeState?.baseagent, name, 'stop'));
|
|
1382
1382
|
}
|
|
1383
1383
|
}
|
|
1384
1384
|
}
|
|
1385
|
-
return
|
|
1385
|
+
return {
|
|
1386
|
+
count: interrupted,
|
|
1387
|
+
barrier: Promise.allSettled(barriers).then(() => undefined),
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
/** 中断指定 agent 所有正在处理中的会话。 */
|
|
1391
|
+
interruptByAgent(agentName) {
|
|
1392
|
+
const interruption = this.beginAgentInterrupt(agentName);
|
|
1393
|
+
void interruption.barrier;
|
|
1394
|
+
return interruption.count;
|
|
1395
|
+
}
|
|
1396
|
+
/** 中断指定 agent 的会话,并等待 runner 中断屏障完成。 */
|
|
1397
|
+
async interruptByAgentAndWait(agentName) {
|
|
1398
|
+
const interruption = this.beginAgentInterrupt(agentName);
|
|
1399
|
+
await interruption.barrier;
|
|
1400
|
+
return interruption.count;
|
|
1386
1401
|
}
|
|
1387
1402
|
// ── Queue query/management methods ──
|
|
1388
1403
|
/**
|