evolcore 0.0.2 → 0.0.4
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 +93 -778
- package/README.md +45 -10
- package/bin/ec-safe-output.js +89 -24
- package/dist/agents/baseagent.js +46 -0
- package/dist/agents/claude-runner.js +198 -48
- package/dist/agents/codex-app-server-client.js +68 -0
- package/dist/agents/codex-runner.js +203 -8
- package/dist/agents/runner-types.js +2 -2
- package/dist/aun/aid/agentmd.js +79 -0
- package/dist/aun/aid/encryption-seed-policy.js +29 -0
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/aid/store.js +2 -5
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +241 -116
- package/dist/channels/daemon.js +18 -4
- package/dist/channels/dingtalk.js +1005 -260
- package/dist/channels/feishu.js +184 -56
- package/dist/channels/qqbot.js +23 -1
- package/dist/channels/wechat.js +303 -155
- package/dist/channels/wecom.js +1132 -164
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +98 -43
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +4 -3
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +242 -129
- package/dist/cli/init.js +70 -31
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +146 -25
- package/dist/cli/version.js +6 -1
- package/dist/config/access-policy-domain.js +38 -0
- package/dist/config/access-policy.js +134 -0
- package/dist/config/builtin-role-templates.js +42 -17
- package/dist/config/builtin-roles.js +31 -6
- package/dist/config/config-field-policy.js +17 -5
- package/dist/config/config-manager.js +409 -29
- package/dist/config/config-operation-service.js +35 -38
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-bind-code.js +274 -0
- package/dist/config/contact-book-store.js +622 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +185 -300
- package/dist/config/contact-operation-service.js +116 -0
- package/dist/config/contact-request-service.js +331 -0
- package/dist/config/peer-role-resolver.js +136 -55
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config/schema-registry.js +49 -24
- package/dist/config-store.js +14 -4
- package/dist/core/auth/agent-delegation.js +105 -11
- package/dist/core/auth/authorization-audit.js +16 -1
- package/dist/core/auth/operation-authorizer.js +10 -0
- package/dist/core/auth/operation-catalog.js +105 -1
- package/dist/core/auth/trigger-authorization.js +15 -0
- package/dist/core/bootstrap-service.js +2 -9
- package/dist/core/channel-loader.js +6 -2
- package/dist/core/command/command-handler.js +113 -21
- package/dist/core/command/connect-menu.js +599 -0
- package/dist/core/command/evol-menu-version-gate.js +38 -0
- package/dist/core/command/group-menu.js +421 -0
- package/dist/core/command/menu-handler.js +382 -100
- package/dist/core/command/menu-protocol.js +8 -2
- package/dist/core/command/menu-token-store.js +102 -0
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-gate.js +1 -1
- package/dist/core/command/slash-handler.js +186 -49
- package/dist/core/daemon-file-cache.js +40 -6
- package/dist/core/event-catalog.js +99 -0
- package/dist/core/evolagent-registry.js +4 -40
- package/dist/core/evolagent.js +20 -9
- package/dist/core/handoff/runtime.js +162 -37
- package/dist/core/handoff/store.js +46 -2
- package/dist/core/handoff/types.js +1 -0
- package/dist/core/inference/text-inference.js +16 -74
- package/dist/core/message/file-markers.js +7 -0
- package/dist/core/message/im-renderer.js +20 -14
- package/dist/core/message/inbound-admission.js +124 -0
- package/dist/core/message/message-bridge.js +670 -57
- package/dist/core/message/message-log.js +1 -0
- package/dist/core/message/message-queue.js +195 -10
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/peer-mode.js +7 -8
- package/dist/core/message/response-engine.js +642 -96
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/approval-gateway.js +17 -10
- package/dist/core/permission/ec-command-parser.js +101 -46
- package/dist/core/permission/tool-policy.js +115 -38
- package/dist/core/protected-paths.js +38 -11
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +86 -8
- package/dist/core/session/session-mapper.js +2 -0
- package/dist/core/session/session-renew.js +125 -69
- package/dist/core/session/session-turn-coordinator.js +5 -1
- package/dist/eck/kit-renderer.js +12 -1
- package/dist/eck/message-renderer.js +79 -1
- package/dist/index.js +285 -94
- package/dist/ipc.js +114 -8
- package/dist/paths.js +3 -0
- package/dist/response-system/config-resolver.js +29 -0
- package/dist/response-system/coordinator.js +8 -32
- package/dist/response-system/engines/v1/proactive-flow.js +1 -1
- package/dist/response-system/index.js +1 -0
- package/dist/response-system/modes/single-session/index.js +7 -4
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +126 -11
- package/dist/trigger/patch.js +12 -2
- package/dist/trigger/scheduler.js +444 -40
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/error-dict.json +7 -0
- package/dist/utils/evolcore-version.js +21 -0
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/logger.js +2 -2
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/stable-semver.js +21 -0
- package/dist/utils/stats.js +33 -9
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/channels/aun.md +4 -13
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/config.md +47 -2
- package/kits/docs/evolcore/contact.md +61 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group-rules.md +46 -4
- package/kits/docs/evolcore/group.md +15 -6
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +12 -6
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +42 -10
- package/kits/docs/path-registry.md +36 -17
- package/kits/eck_manifest.json +12 -0
- package/kits/eck_message_manifest.json +12 -1
- package/kits/migrations/migrate-contact-book-v2.mjs +754 -0
- package/kits/rules/01-overview.md +17 -7
- package/kits/rules/02-navigation.md +38 -18
- package/kits/rules/03-identity.md +28 -24
- package/kits/rules/04-relation.md +44 -28
- package/kits/rules/05-venue.md +31 -15
- package/kits/rules/06-channel.md +11 -7
- package/kits/schemas/_meta.json +21 -7
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/agent-config.schema.7.json +303 -0
- package/kits/schemas/agent-config.schema.8.json +304 -0
- package/kits/schemas/agent-config.schema.9.json +374 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/contact-book.schema.3.json +67 -0
- package/kits/schemas/daemon.schema.1.json +3 -2
- package/kits/schemas/daemon.schema.2.json +101 -0
- package/kits/schemas/daemon.schema.3.json +123 -0
- package/kits/schemas/defaults.schema.2.json +85 -0
- package/kits/schemas/defaults.schema.3.json +73 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/relation-config.schema.6.json +46 -0
- package/kits/schemas/relation-config.schema.7.json +59 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/schemas/single-session.schema.2.json +57 -0
- package/kits/templates/message-fragments/handoff-context-to-target.md +9 -0
- package/kits/templates/message-fragments/handoff-request-to-target.md +14 -8
- package/kits/templates/message-fragments/handoff-response-to-origin.md +5 -6
- package/kits/templates/roles/admin.json +47 -0
- package/kits/templates/roles/member.json +5 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +5 -0
- package/kits/templates/system-fragments/channel.md +12 -2
- package/kits/templates/system-fragments/commands.md +3 -1
- package/kits/templates/system-fragments/identity.md +3 -1
- package/kits/templates/system-fragments/relation.md +1 -1
- package/kits/templates/system-fragments/session.md +6 -2
- package/package.json +7 -5
- package/MIGRATION-0.5.0.md +0 -378
- package/ROLE_ACCESS_CONTROL.md +0 -174
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -2,7 +2,8 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { EvolAgent } from './evolagent.js';
|
|
4
4
|
import { logger } from '../utils/logger.js';
|
|
5
|
-
import {
|
|
5
|
+
import { agentPersonalDir } from '../paths.js';
|
|
6
|
+
import { invalidateAgentDisplayName, resolveAgentDisplayName } from '../aun/aid/agentmd.js';
|
|
6
7
|
import { loadAllAgents, ensureAgentDirSkeleton, loadAgent, validateAgentConfig, } from '../config-store.js';
|
|
7
8
|
import { resolveEffective } from '../config/config-manager.js';
|
|
8
9
|
// ── Channel Fingerprint ───────────────────────────────────────────────────
|
|
@@ -129,7 +130,6 @@ export class EvolAgentRegistry {
|
|
|
129
130
|
aid: raw.aid,
|
|
130
131
|
enabled: raw.enabled,
|
|
131
132
|
owners: raw.owners,
|
|
132
|
-
admins: raw.admins,
|
|
133
133
|
channels: Array.isArray(raw.channels) ? raw.channels : [],
|
|
134
134
|
active_baseagent: raw.active_baseagent,
|
|
135
135
|
baseagents: raw.baseagents,
|
|
@@ -211,9 +211,8 @@ export class EvolAgentRegistry {
|
|
|
211
211
|
return [...this.agents.values()].map(a => this.toInfo(a));
|
|
212
212
|
}
|
|
213
213
|
invalidateAgentDisplayCache(aid) {
|
|
214
|
-
|
|
214
|
+
invalidateAgentDisplayName(aid);
|
|
215
215
|
this.personalNameCache.delete(aid);
|
|
216
|
-
this.displayNamePending.delete(aid);
|
|
217
216
|
}
|
|
218
217
|
runnableAgents() {
|
|
219
218
|
return [...this.agents.values()].filter(a => a.status === 'stopped');
|
|
@@ -443,43 +442,8 @@ export class EvolAgentRegistry {
|
|
|
443
442
|
}
|
|
444
443
|
return null;
|
|
445
444
|
}
|
|
446
|
-
displayNameCache = new Map();
|
|
447
|
-
displayNamePending = new Set();
|
|
448
445
|
resolveDisplayName(aid) {
|
|
449
|
-
|
|
450
|
-
if (cached)
|
|
451
|
-
return cached;
|
|
452
|
-
try {
|
|
453
|
-
const mdPath = agentMdPath(aid);
|
|
454
|
-
if (fs.existsSync(mdPath)) {
|
|
455
|
-
const content = fs.readFileSync(mdPath, 'utf-8');
|
|
456
|
-
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
457
|
-
if (fm) {
|
|
458
|
-
const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
459
|
-
if (nm?.[1]) {
|
|
460
|
-
this.displayNameCache.set(aid, nm[1]);
|
|
461
|
-
return nm[1];
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
catch { }
|
|
467
|
-
if (!this.displayNamePending.has(aid)) {
|
|
468
|
-
this.displayNamePending.add(aid);
|
|
469
|
-
import('../aun/aid/index.js').then(({ agentmdGet }) => {
|
|
470
|
-
agentmdGet(aid).then(content => {
|
|
471
|
-
if (typeof content === 'string') {
|
|
472
|
-
const fm = content.match(/^---\n([\s\S]*?)\n---/);
|
|
473
|
-
if (fm) {
|
|
474
|
-
const nm = fm[1].match(/^name:\s*["']?(.+?)["']?\s*$/m);
|
|
475
|
-
if (nm?.[1])
|
|
476
|
-
this.displayNameCache.set(aid, nm[1]);
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
}).catch(() => { }).finally(() => this.displayNamePending.delete(aid));
|
|
480
|
-
}).catch(() => { this.displayNamePending.delete(aid); });
|
|
481
|
-
}
|
|
482
|
-
return undefined;
|
|
446
|
+
return resolveAgentDisplayName(aid);
|
|
483
447
|
}
|
|
484
448
|
personalNameCache = new Map();
|
|
485
449
|
resolvePersonalName(aid) {
|
package/dist/core/evolagent.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
|
-
import { formatChannelKey } from './channel-loader.js';
|
|
2
|
+
import { formatChannelKey, tryParseChannelKey } from './channel-loader.js';
|
|
3
3
|
import { agentPersonalDir } from '../paths.js';
|
|
4
4
|
import { fileCache } from './daemon-file-cache.js';
|
|
5
5
|
import { ConfigTarget, read as cfgRead, write as cfgWrite, ensureFile as cfgEnsure, resolveEffective } from '../config/config-manager.js';
|
|
6
6
|
import { withLifecycleForWrite } from '../config/lifecycle.js';
|
|
7
|
+
import { isStaticAgentAdmin, isStaticAgentOwner } from '../config/peer-role-resolver.js';
|
|
8
|
+
import { resolvePrincipal } from '../config/contact-book.js';
|
|
7
9
|
/**
|
|
8
10
|
* EvolAgent —— 一个 self-agent 的运行时表示。
|
|
9
11
|
*
|
|
@@ -123,19 +125,28 @@ export class EvolAgent {
|
|
|
123
125
|
/**
|
|
124
126
|
* Check if a user has owner role for this agent (private scope).
|
|
125
127
|
*/
|
|
126
|
-
isOwner(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return false;
|
|
128
|
+
isOwner(channelKey, userId) {
|
|
129
|
+
const principalId = this.managementPrincipal(channelKey, userId);
|
|
130
|
+
return !!principalId && isStaticAgentOwner(this.aid, principalId);
|
|
130
131
|
}
|
|
131
132
|
/**
|
|
132
133
|
* Check if a user has admin role for this agent (private scope).
|
|
133
|
-
*
|
|
134
|
+
* Owner remains in Agent config; admin is resolved from the direct AUN relation.
|
|
134
135
|
*/
|
|
135
|
-
isAdmin(
|
|
136
|
-
|
|
136
|
+
isAdmin(channelKey, userId) {
|
|
137
|
+
const principalId = this.managementPrincipal(channelKey, userId);
|
|
138
|
+
if (!principalId)
|
|
139
|
+
return false;
|
|
140
|
+
if (isStaticAgentOwner(this.aid, principalId))
|
|
137
141
|
return true;
|
|
138
|
-
return this.
|
|
142
|
+
return isStaticAgentAdmin(this.aid, principalId);
|
|
143
|
+
}
|
|
144
|
+
managementPrincipal(channelKey, userId) {
|
|
145
|
+
const channelType = tryParseChannelKey(channelKey)?.type ?? String(channelKey || '').split('#')[0];
|
|
146
|
+
const resolution = resolvePrincipal(this.aid, channelType, userId, channelKey);
|
|
147
|
+
return resolution.principalStatus === 'native_aid' || resolution.principalStatus === 'mapped'
|
|
148
|
+
? resolution.principalId ?? null
|
|
149
|
+
: null;
|
|
139
150
|
}
|
|
140
151
|
/**
|
|
141
152
|
* Get the first owner of this agent (private scope).
|
|
@@ -5,6 +5,10 @@ import { KeyedFairMutex } from './mutex.js';
|
|
|
5
5
|
import { HandoffStore } from './store.js';
|
|
6
6
|
import { createRootCausation, deriveCausation, normalizeCausation } from '../causation/context.js';
|
|
7
7
|
import { recordCausationSpan } from '../causation/audit.js';
|
|
8
|
+
import { logger } from '../../utils/logger.js';
|
|
9
|
+
const HANDOFF_STALE_WARN_MS = 72 * 60 * 60 * 1000;
|
|
10
|
+
const HANDOFF_LARGE_AGGREGATE_COUNT = 10;
|
|
11
|
+
const HANDOFF_LARGE_AGGREGATE_CHARS = 50_000;
|
|
8
12
|
export class HandoffTargetBlockedError extends Error {
|
|
9
13
|
blockingHandoffId;
|
|
10
14
|
attentionReason;
|
|
@@ -25,6 +29,20 @@ export class HandoffDeliveryFailedError extends Error {
|
|
|
25
29
|
this.name = 'HandoffDeliveryFailedError';
|
|
26
30
|
}
|
|
27
31
|
}
|
|
32
|
+
export class HandoffReturnPolicyRequiredError extends Error {
|
|
33
|
+
code = 'HANDOFF_RETURN_POLICY_REQUIRED';
|
|
34
|
+
constructor() {
|
|
35
|
+
super('跨会话 ec msg/group send 必须显式指定 --return none|required');
|
|
36
|
+
this.name = 'HandoffReturnPolicyRequiredError';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class TriggerHandoffRequiredForbiddenError extends Error {
|
|
40
|
+
code = 'TRIGGER_HANDOFF_REQUIRED_FORBIDDEN';
|
|
41
|
+
constructor() {
|
|
42
|
+
super('Trigger 的跨会话 ec msg/group send 只允许 --return none,不能使用 required');
|
|
43
|
+
this.name = 'TriggerHandoffRequiredForbiddenError';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
28
46
|
export class HandoffRuntime {
|
|
29
47
|
sessionManager;
|
|
30
48
|
messageQueue;
|
|
@@ -32,6 +50,8 @@ export class HandoffRuntime {
|
|
|
32
50
|
mutexes = new KeyedFairMutex();
|
|
33
51
|
dispatcher;
|
|
34
52
|
now;
|
|
53
|
+
staleWarned = new Set();
|
|
54
|
+
largeAggregateWarned = new Set();
|
|
35
55
|
constructor(sessionManager, messageQueue, sender, store = new HandoffStore(), options = {}) {
|
|
36
56
|
this.sessionManager = sessionManager;
|
|
37
57
|
this.messageQueue = messageQueue;
|
|
@@ -56,6 +76,11 @@ export class HandoffRuntime {
|
|
|
56
76
|
if (target.id === origin.id) {
|
|
57
77
|
return { crossSession: false, targetSession: target };
|
|
58
78
|
}
|
|
79
|
+
if (!input.explicitReturnPolicy)
|
|
80
|
+
throw new HandoffReturnPolicyRequiredError();
|
|
81
|
+
if (input.explicitReturnPolicy === 'required' && input.causation?.trigger?.path.length) {
|
|
82
|
+
throw new TriggerHandoffRequiredForbiddenError();
|
|
83
|
+
}
|
|
59
84
|
await this.dispatcher.expireTargetQueued(input.selfAid, target.id);
|
|
60
85
|
throwIfAborted(input.signal);
|
|
61
86
|
const head = this.store.listByTarget(input.selfAid, target.id, 'queued')[0];
|
|
@@ -91,7 +116,8 @@ export class HandoffRuntime {
|
|
|
91
116
|
}
|
|
92
117
|
hasOpenTarget(selfAid, targetSessionId) {
|
|
93
118
|
return this.store.listByTarget(selfAid, targetSessionId)
|
|
94
|
-
.some(instance => instance.state === 'target_sent'
|
|
119
|
+
.some(instance => instance.state === 'target_sent'
|
|
120
|
+
|| (instance.state === 'return_pending' && instance.return_policy === 'required'));
|
|
95
121
|
}
|
|
96
122
|
async bindReply(input) {
|
|
97
123
|
const key = `${input.selfAid}\u0000${input.targetSessionId}`;
|
|
@@ -105,6 +131,8 @@ export class HandoffRuntime {
|
|
|
105
131
|
responseMessageId: input.responseMessageId,
|
|
106
132
|
refMessageId: input.refMessageId,
|
|
107
133
|
});
|
|
134
|
+
for (const instance of attached)
|
|
135
|
+
this.warnIfStale(instance);
|
|
108
136
|
const handoffIds = attached.map(instance => instance.handoff_id);
|
|
109
137
|
return {
|
|
110
138
|
candidate: handoffIds.length > 0,
|
|
@@ -157,6 +185,7 @@ export class HandoffRuntime {
|
|
|
157
185
|
messageId,
|
|
158
186
|
timestamp: Date.now(),
|
|
159
187
|
source: 'handoff',
|
|
188
|
+
boundSessionId: origin.id,
|
|
160
189
|
causation: instance.return_causation
|
|
161
190
|
? deriveCausation(instance.return_causation)
|
|
162
191
|
: instance.causation
|
|
@@ -186,55 +215,120 @@ export class HandoffRuntime {
|
|
|
186
215
|
}
|
|
187
216
|
}
|
|
188
217
|
buildPromptItem(message) {
|
|
189
|
-
|
|
190
|
-
const selfAid = message.selfAID;
|
|
191
|
-
if (!delivery || !selfAid)
|
|
192
|
-
return null;
|
|
193
|
-
const instance = this.store.get(selfAid, delivery.handoffId);
|
|
194
|
-
if (!instance)
|
|
195
|
-
return null;
|
|
196
|
-
if (delivery.direction === 'target' && instance.state !== 'return_pending')
|
|
197
|
-
return null;
|
|
198
|
-
if (delivery.direction === 'origin' && instance.state !== 'origin_delivered')
|
|
199
|
-
return null;
|
|
200
|
-
return {
|
|
201
|
-
kind: 'handoff',
|
|
202
|
-
peerId: message.peerId,
|
|
203
|
-
peerName: message.peerName,
|
|
204
|
-
peerType: message.peerType,
|
|
205
|
-
content: message.content,
|
|
206
|
-
timestamp: message.timestamp,
|
|
207
|
-
causation: message.causation,
|
|
208
|
-
handoff: {
|
|
209
|
-
kind: delivery.direction === 'target' ? 'request_to_target' : 'response_to_origin',
|
|
210
|
-
handoffId: instance.handoff_id,
|
|
211
|
-
previousContent: this.requestSummary(instance),
|
|
212
|
-
previousMessageId: instance.target_message_id,
|
|
213
|
-
returnPolicy: instance.return_policy,
|
|
214
|
-
},
|
|
215
|
-
};
|
|
218
|
+
return this.buildPromptItems(message)[0] ?? null;
|
|
216
219
|
}
|
|
217
220
|
buildPromptItems(message) {
|
|
218
221
|
const delivery = message.handoffDelivery;
|
|
219
|
-
|
|
222
|
+
const selfAid = message.selfAID;
|
|
223
|
+
if (!delivery || !selfAid)
|
|
220
224
|
return [];
|
|
221
225
|
const ids = delivery.handoffIds?.length ? delivery.handoffIds : [delivery.handoffId];
|
|
222
|
-
const
|
|
226
|
+
const instances = [];
|
|
223
227
|
for (const handoffId of ids) {
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
const instance = this.store.get(selfAid, handoffId);
|
|
229
|
+
if (!instance || instance.attention_required)
|
|
230
|
+
continue;
|
|
231
|
+
if (delivery.direction === 'target') {
|
|
232
|
+
const expectedState = instance.return_policy === 'none'
|
|
233
|
+
? 'target_context_pending'
|
|
234
|
+
: 'return_pending';
|
|
235
|
+
if (instance.state !== expectedState)
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
else if (instance.state !== 'origin_delivered') {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
instances.push(instance);
|
|
242
|
+
}
|
|
243
|
+
if (instances.length === 0)
|
|
244
|
+
return [];
|
|
245
|
+
if (delivery.direction === 'origin') {
|
|
246
|
+
this.warnIfLargeAggregate(message, instances, 'origin');
|
|
247
|
+
const first = instances[0];
|
|
248
|
+
return [{
|
|
249
|
+
kind: 'handoff',
|
|
250
|
+
peerId: message.peerId,
|
|
251
|
+
peerName: message.peerName,
|
|
252
|
+
peerType: message.peerType,
|
|
253
|
+
content: message.content,
|
|
254
|
+
timestamp: message.timestamp,
|
|
255
|
+
causation: message.causation,
|
|
256
|
+
handoff: {
|
|
257
|
+
kind: 'response_to_origin',
|
|
258
|
+
handoffId: first.handoff_id,
|
|
259
|
+
handoffIds: instances.map(instance => instance.handoff_id),
|
|
260
|
+
originEntries: instances.map(instance => ({
|
|
261
|
+
previousContent: this.requestSummary(instance),
|
|
262
|
+
responseContent: instance.return_content ?? '',
|
|
263
|
+
previousMessageId: instance.target_message_id,
|
|
264
|
+
})),
|
|
265
|
+
previousContent: this.requestSummary(first),
|
|
266
|
+
previousMessageId: first.target_message_id,
|
|
267
|
+
returnPolicy: first.return_policy,
|
|
268
|
+
},
|
|
269
|
+
}];
|
|
230
270
|
}
|
|
231
|
-
|
|
271
|
+
const requiredEntries = instances
|
|
272
|
+
.filter(instance => instance.return_policy === 'required')
|
|
273
|
+
.map(instance => ({
|
|
274
|
+
handoffId: instance.handoff_id,
|
|
275
|
+
previousContent: this.requestSummary(instance),
|
|
276
|
+
previousMessageId: instance.target_message_id,
|
|
277
|
+
}));
|
|
278
|
+
const contextEntries = instances
|
|
279
|
+
.filter(instance => instance.return_policy === 'none')
|
|
280
|
+
.map(instance => ({
|
|
281
|
+
previousContent: this.requestSummary(instance),
|
|
282
|
+
previousMessageId: instance.target_message_id,
|
|
283
|
+
}));
|
|
284
|
+
this.warnIfLargeAggregate(message, instances, 'target');
|
|
285
|
+
const first = instances[0];
|
|
286
|
+
return [{
|
|
287
|
+
kind: 'handoff',
|
|
288
|
+
peerId: message.peerId,
|
|
289
|
+
peerName: message.peerName,
|
|
290
|
+
peerType: message.peerType,
|
|
291
|
+
content: message.content,
|
|
292
|
+
timestamp: message.timestamp,
|
|
293
|
+
causation: message.causation,
|
|
294
|
+
handoff: {
|
|
295
|
+
kind: 'request_to_target',
|
|
296
|
+
handoffId: first.handoff_id,
|
|
297
|
+
handoffIds: instances.map(instance => instance.handoff_id),
|
|
298
|
+
requiredEntries,
|
|
299
|
+
contextEntries,
|
|
300
|
+
previousContent: this.requestSummary(first),
|
|
301
|
+
previousMessageId: first.target_message_id,
|
|
302
|
+
returnPolicy: requiredEntries.length > 0 ? 'required' : 'none',
|
|
303
|
+
},
|
|
304
|
+
}];
|
|
232
305
|
}
|
|
233
306
|
completeOriginContext(selfAid, handoffId) {
|
|
234
307
|
const instance = this.store.get(selfAid, handoffId);
|
|
235
308
|
if (instance?.state === 'origin_delivered')
|
|
236
309
|
this.store.completeOriginContext(selfAid, handoffId);
|
|
237
310
|
}
|
|
311
|
+
completeTargetContext(selfAid, handoffId) {
|
|
312
|
+
const instance = this.store.get(selfAid, handoffId);
|
|
313
|
+
if (instance?.state === 'target_context_pending' && instance.return_policy === 'none') {
|
|
314
|
+
this.store.completeTargetContext(selfAid, handoffId);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
validateTargetSessionBinding(selfAid, handoffIds, executionSessionId) {
|
|
318
|
+
const invalidHandoffIds = [];
|
|
319
|
+
for (const handoffId of handoffIds) {
|
|
320
|
+
const instance = this.store.get(selfAid, handoffId);
|
|
321
|
+
if (!instance
|
|
322
|
+
|| instance.target_session_id !== executionSessionId
|
|
323
|
+
|| instance.consumed_target_session_id !== executionSessionId) {
|
|
324
|
+
invalidHandoffIds.push(handoffId);
|
|
325
|
+
if (instance && !instance.attention_required) {
|
|
326
|
+
this.store.markAttention(selfAid, handoffId, 'HANDOFF_SESSION_BINDING_MISMATCH');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { ok: invalidHandoffIds.length === 0, invalidHandoffIds };
|
|
331
|
+
}
|
|
238
332
|
status(selfAid, handoffId) {
|
|
239
333
|
return this.store.status(selfAid, handoffId);
|
|
240
334
|
}
|
|
@@ -276,6 +370,10 @@ export class HandoffRuntime {
|
|
|
276
370
|
async recover(selfAids) {
|
|
277
371
|
const originEnqueues = [];
|
|
278
372
|
for (const selfAid of selfAids) {
|
|
373
|
+
// v1 initially represented return=none as return_pending and required an
|
|
374
|
+
// empty manual return. It has already been shown to a target turn, so
|
|
375
|
+
// finish it before version reconciliation instead of prompting again.
|
|
376
|
+
this.store.migrateLegacyNonePending(selfAid, this.now?.());
|
|
279
377
|
this.expireQueued([selfAid]);
|
|
280
378
|
const events = this.store.events(selfAid);
|
|
281
379
|
const lastSendEvent = new Map();
|
|
@@ -285,6 +383,7 @@ export class HandoffRuntime {
|
|
|
285
383
|
}
|
|
286
384
|
}
|
|
287
385
|
for (const instance of this.store.list(selfAid)) {
|
|
386
|
+
this.warnIfStale(instance);
|
|
288
387
|
const versionedEvents = events.filter(event => event.handoff_id === instance.handoff_id && event.to_version !== undefined);
|
|
289
388
|
const latestEventVersion = versionedEvents.reduce((latest, event) => Math.max(latest, event.to_version ?? 0), 0);
|
|
290
389
|
if (latestEventVersion !== instance.version) {
|
|
@@ -313,6 +412,32 @@ export class HandoffRuntime {
|
|
|
313
412
|
}
|
|
314
413
|
await Promise.all(originEnqueues);
|
|
315
414
|
}
|
|
415
|
+
warnIfStale(instance) {
|
|
416
|
+
if (instance.state !== 'target_sent' && instance.state !== 'return_pending')
|
|
417
|
+
return;
|
|
418
|
+
const ageMs = (this.now?.() ?? Date.now()) - instance.created_at;
|
|
419
|
+
const key = instance.handoff_id;
|
|
420
|
+
if (ageMs < HANDOFF_STALE_WARN_MS || this.staleWarned.has(key))
|
|
421
|
+
return;
|
|
422
|
+
this.staleWarned.add(key);
|
|
423
|
+
logger.warn(`[Handoff] stale open handoff id=${instance.handoff_id}`
|
|
424
|
+
+ ` state=${instance.state} returnPolicy=${instance.return_policy}`
|
|
425
|
+
+ ` ageHours=${Math.floor(ageMs / 3_600_000)}`
|
|
426
|
+
+ ` originSession=${instance.origin_session_id} targetSession=${instance.target_session_id}`);
|
|
427
|
+
}
|
|
428
|
+
warnIfLargeAggregate(message, instances, direction) {
|
|
429
|
+
const chars = message.content.length
|
|
430
|
+
+ instances.reduce((total, instance) => total + this.requestSummary(instance).length, 0);
|
|
431
|
+
if (instances.length < HANDOFF_LARGE_AGGREGATE_COUNT && chars < HANDOFF_LARGE_AGGREGATE_CHARS)
|
|
432
|
+
return;
|
|
433
|
+
const key = `${message.messageId ?? '<none>'}:${instances.map(instance => instance.handoff_id).join(',')}`;
|
|
434
|
+
if (this.largeAggregateWarned.has(key))
|
|
435
|
+
return;
|
|
436
|
+
this.largeAggregateWarned.add(key);
|
|
437
|
+
logger.warn(`[Handoff] large prompt aggregate direction=${direction}`
|
|
438
|
+
+ ` session=${direction === 'target' ? instances[0].target_session_id : instances[0].origin_session_id}`
|
|
439
|
+
+ ` count=${instances.length} chars=${chars} responseMessage=${message.messageId ?? '<none>'}`);
|
|
440
|
+
}
|
|
316
441
|
requestSummary(instance) {
|
|
317
442
|
return classifyAunPayloadForLog(instance.request.payload).content || '[payload]';
|
|
318
443
|
}
|
|
@@ -272,7 +272,8 @@ export class HandoffStore {
|
|
|
272
272
|
}
|
|
273
273
|
attachSessionContext(input) {
|
|
274
274
|
const open = this.listByTarget(input.selfAid, input.targetSessionId)
|
|
275
|
-
.filter(item => (item.state === 'target_sent'
|
|
275
|
+
.filter(item => (item.state === 'target_sent'
|
|
276
|
+
|| (item.state === 'return_pending' && item.return_policy === 'required')) && !item.attention_required)
|
|
276
277
|
.sort((left, right) => {
|
|
277
278
|
const leftRef = input.refMessageId && left.target_message_id === input.refMessageId ? 0 : 1;
|
|
278
279
|
const rightRef = input.refMessageId && right.target_message_id === input.refMessageId ? 0 : 1;
|
|
@@ -283,7 +284,9 @@ export class HandoffStore {
|
|
|
283
284
|
const previousVersion = instance.version;
|
|
284
285
|
const firstAttachment = instance.state === 'target_sent';
|
|
285
286
|
if (firstAttachment) {
|
|
286
|
-
instance.state = '
|
|
287
|
+
instance.state = instance.return_policy === 'none'
|
|
288
|
+
? 'target_context_pending'
|
|
289
|
+
: 'return_pending';
|
|
287
290
|
instance.response_message_id = input.responseMessageId;
|
|
288
291
|
instance.consumed_at = now;
|
|
289
292
|
instance.consumed_target_session_id = input.targetSessionId;
|
|
@@ -308,6 +311,47 @@ export class HandoffStore {
|
|
|
308
311
|
bindExactReply(input) {
|
|
309
312
|
return this.attachSessionContext(input)[0] ?? null;
|
|
310
313
|
}
|
|
314
|
+
completeTargetContext(selfAid, handoffId, now = Date.now()) {
|
|
315
|
+
const instance = this.requireState(selfAid, handoffId, 'target_context_pending');
|
|
316
|
+
if (instance.return_policy !== 'none') {
|
|
317
|
+
throw new Error('target context auto-completion requires return=none');
|
|
318
|
+
}
|
|
319
|
+
const previous = instance.version;
|
|
320
|
+
instance.state = 'completed';
|
|
321
|
+
instance.version++;
|
|
322
|
+
instance.updated_at = now;
|
|
323
|
+
instance.completed_at = now;
|
|
324
|
+
this.appendEvent(selfAid, 'target_context_consumed', handoffId, {
|
|
325
|
+
operationKey: instance.response_message_id ?? undefined,
|
|
326
|
+
fromVersion: previous,
|
|
327
|
+
toVersion: instance.version,
|
|
328
|
+
now,
|
|
329
|
+
});
|
|
330
|
+
this.write(selfAid, instance);
|
|
331
|
+
return instance;
|
|
332
|
+
}
|
|
333
|
+
migrateLegacyNonePending(selfAid, now = Date.now()) {
|
|
334
|
+
const migrated = [];
|
|
335
|
+
for (const instance of this.list(selfAid)) {
|
|
336
|
+
if (instance.return_policy !== 'none' || instance.state !== 'return_pending')
|
|
337
|
+
continue;
|
|
338
|
+
const previous = instance.version;
|
|
339
|
+
instance.state = 'completed';
|
|
340
|
+
instance.version++;
|
|
341
|
+
instance.updated_at = now;
|
|
342
|
+
instance.completed_at = now;
|
|
343
|
+
this.appendEvent(selfAid, 'target_context_consumed', instance.handoff_id, {
|
|
344
|
+
operationKey: instance.response_message_id ?? undefined,
|
|
345
|
+
fromVersion: previous,
|
|
346
|
+
toVersion: instance.version,
|
|
347
|
+
details: { migration: true, previous_state: 'return_pending' },
|
|
348
|
+
now,
|
|
349
|
+
});
|
|
350
|
+
this.write(selfAid, instance);
|
|
351
|
+
migrated.push(instance);
|
|
352
|
+
}
|
|
353
|
+
return migrated;
|
|
354
|
+
}
|
|
311
355
|
returnHandoff(input) {
|
|
312
356
|
const selected = this.selectReturnCandidate(input);
|
|
313
357
|
if ('ok' in selected)
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const base = (baseUrl || defaultBaseUrl).replace(/\/+$/, '');
|
|
1
|
+
import { normalizeBaseagent, resolveAnthropicDirectConfig, resolveOpenaiDirectConfig, } from '../../agents/baseagent.js';
|
|
2
|
+
function apiEndpoint(baseUrl, resource) {
|
|
3
|
+
const configured = baseUrl?.trim();
|
|
4
|
+
if (!configured)
|
|
5
|
+
throw new Error('Direct text inference baseUrl is not configured');
|
|
6
|
+
const base = configured.replace(/\/+$/, '');
|
|
8
7
|
return base.endsWith('/v1') ? `${base}/${resource}` : `${base}/v1/${resource}`;
|
|
9
8
|
}
|
|
10
9
|
async function parseError(response) {
|
|
@@ -28,53 +27,8 @@ export class AnthropicTextInferenceProvider {
|
|
|
28
27
|
constructor(config) {
|
|
29
28
|
this.config = config;
|
|
30
29
|
}
|
|
31
|
-
async listModels(signal) {
|
|
32
|
-
const cacheKey = this.modelListCacheKey();
|
|
33
|
-
const cached = anthropicModelListCache.get(cacheKey);
|
|
34
|
-
if (cached && Date.now() - cached.fetchedAt < MODEL_LIST_TTL_MS)
|
|
35
|
-
return [...cached.models];
|
|
36
|
-
const models = [];
|
|
37
|
-
const seen = new Set();
|
|
38
|
-
let afterId;
|
|
39
|
-
for (let page = 0; page < MODEL_LIST_MAX_PAGES; page++) {
|
|
40
|
-
const endpoint = new URL(apiEndpoint(this.config.baseUrl, 'https://api.anthropic.com', 'models'));
|
|
41
|
-
endpoint.searchParams.set('limit', '100');
|
|
42
|
-
if (afterId)
|
|
43
|
-
endpoint.searchParams.set('after_id', afterId);
|
|
44
|
-
const response = await fetch(endpoint, {
|
|
45
|
-
method: 'GET',
|
|
46
|
-
signal,
|
|
47
|
-
headers: this.headers(),
|
|
48
|
-
});
|
|
49
|
-
if (!response.ok)
|
|
50
|
-
throw await parseError(response);
|
|
51
|
-
const body = await response.json();
|
|
52
|
-
const entries = Array.isArray(body.data)
|
|
53
|
-
? body.data
|
|
54
|
-
: (Array.isArray(body.models) ? body.models : []);
|
|
55
|
-
for (const entry of entries) {
|
|
56
|
-
const id = typeof entry === 'string'
|
|
57
|
-
? entry
|
|
58
|
-
: entry?.id || entry?.name || entry?.model;
|
|
59
|
-
if (typeof id !== 'string' || !id.trim() || seen.has(id))
|
|
60
|
-
continue;
|
|
61
|
-
seen.add(id);
|
|
62
|
-
models.push(id);
|
|
63
|
-
}
|
|
64
|
-
if (body.has_more !== true)
|
|
65
|
-
break;
|
|
66
|
-
const next = typeof body.last_id === 'string' && body.last_id ? body.last_id : undefined;
|
|
67
|
-
if (!next || next === afterId)
|
|
68
|
-
throw new Error('Claude model list pagination returned no next cursor');
|
|
69
|
-
afterId = next;
|
|
70
|
-
}
|
|
71
|
-
if (models.length === 0)
|
|
72
|
-
throw new Error('Claude model list API returned no models');
|
|
73
|
-
anthropicModelListCache.set(cacheKey, { models, fetchedAt: Date.now() });
|
|
74
|
-
return [...models];
|
|
75
|
-
}
|
|
76
30
|
async completeText(request) {
|
|
77
|
-
const response = await fetch(apiEndpoint(this.config.baseUrl, '
|
|
31
|
+
const response = await fetch(apiEndpoint(this.config.baseUrl, 'messages'), {
|
|
78
32
|
method: 'POST',
|
|
79
33
|
signal: request.signal,
|
|
80
34
|
headers: this.headers(),
|
|
@@ -83,6 +37,7 @@ export class AnthropicTextInferenceProvider {
|
|
|
83
37
|
max_tokens: 256,
|
|
84
38
|
system: request.system,
|
|
85
39
|
messages: [{ role: 'user', content: request.input }],
|
|
40
|
+
...(request.effort ? { output_config: { effort: request.effort } } : {}),
|
|
86
41
|
}),
|
|
87
42
|
});
|
|
88
43
|
if (!response.ok)
|
|
@@ -106,14 +61,6 @@ export class AnthropicTextInferenceProvider {
|
|
|
106
61
|
Authorization: `Bearer ${this.config.apiKey}`,
|
|
107
62
|
};
|
|
108
63
|
}
|
|
109
|
-
modelListCacheKey() {
|
|
110
|
-
const endpoint = apiEndpoint(this.config.baseUrl, 'https://api.anthropic.com', 'models');
|
|
111
|
-
const credential = createHash('sha256').update(this.config.apiKey).digest('hex');
|
|
112
|
-
return `${endpoint}\0${credential}`;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
export function _resetTextInferenceModelCacheForTests() {
|
|
116
|
-
anthropicModelListCache.clear();
|
|
117
64
|
}
|
|
118
65
|
export class OpenAITextInferenceProvider {
|
|
119
66
|
config;
|
|
@@ -122,7 +69,7 @@ export class OpenAITextInferenceProvider {
|
|
|
122
69
|
this.config = config;
|
|
123
70
|
}
|
|
124
71
|
async completeText(request) {
|
|
125
|
-
const response = await fetch(apiEndpoint(this.config.baseUrl, '
|
|
72
|
+
const response = await fetch(apiEndpoint(this.config.baseUrl, 'responses'), {
|
|
126
73
|
method: 'POST',
|
|
127
74
|
signal: request.signal,
|
|
128
75
|
headers: {
|
|
@@ -155,19 +102,14 @@ export class OpenAITextInferenceProvider {
|
|
|
155
102
|
}
|
|
156
103
|
}
|
|
157
104
|
export function createTextInferenceProvider(baseagent, config) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const synthetic = { agents: { claude: override } };
|
|
163
|
-
return new AnthropicTextInferenceProvider(resolveAnthropicConfig(synthetic, override));
|
|
105
|
+
const canonical = normalizeBaseagent(baseagent).canonical;
|
|
106
|
+
if (canonical === 'claude') {
|
|
107
|
+
const resolved = resolveAnthropicDirectConfig(config.baseagents?.claude);
|
|
108
|
+
return resolved ? new AnthropicTextInferenceProvider(resolved) : undefined;
|
|
164
109
|
}
|
|
165
|
-
if (
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
return undefined;
|
|
169
|
-
const synthetic = { agents: { codex: override } };
|
|
170
|
-
return new OpenAITextInferenceProvider(resolveOpenaiConfig(synthetic, override));
|
|
110
|
+
if (canonical === 'codex') {
|
|
111
|
+
const resolved = resolveOpenaiDirectConfig(config.baseagents?.codex);
|
|
112
|
+
return resolved ? new OpenAITextInferenceProvider(resolved) : undefined;
|
|
171
113
|
}
|
|
172
114
|
return undefined;
|
|
173
115
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A file marker may contain surrounding whitespace inside its brackets so
|
|
3
|
+
* formatted responses such as `[ SEND_FILE:./report.md ]` remain actionable.
|
|
4
|
+
*/
|
|
5
|
+
export function createSendFileMarkerPattern() {
|
|
6
|
+
return /\[\s*SEND_FILE:(?:(\w+):)?([^\]]*?\S[^\]]*?)\s*\]/g;
|
|
7
|
+
}
|