@evomap/evolver-proxy 2.0.0-beta.1 → 2.0.0-beta.2
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/dist/bin/evolver-llm-proxy.js +0 -0
- package/dist/bin/evolver-proxy.js +4 -0
- package/dist/bin/proxySettings.d.ts +2 -0
- package/dist/bin/proxySettings.js +8 -1
- package/dist/daemon/collaborationFacade.d.ts +56 -0
- package/dist/daemon/collaborationFacade.js +877 -0
- package/dist/daemon/proxyDaemon.d.ts +3 -0
- package/dist/daemon/proxyDaemon.js +111 -0
- package/dist/daemon/selectHub.js +17 -1
- package/dist/private/adapterLoader.d.ts +6 -1
- package/dist/private/adapterLoader.js +14 -3
- package/dist/router/messagesRoute.d.ts +13 -0
- package/dist/router/messagesRoute.js +56 -0
- package/dist/sync/engine.d.ts +6 -5
- package/dist/sync/engine.js +102 -58
- package/package.json +4 -3
|
@@ -56,6 +56,8 @@ export interface ProxyDaemonDeps {
|
|
|
56
56
|
dir: string;
|
|
57
57
|
env?: NodeJS.ProcessEnv;
|
|
58
58
|
};
|
|
59
|
+
/** V1 collaboration task operations are synchronous; tests may shorten the Hub timeout. */
|
|
60
|
+
collaborationOperationTimeoutMs?: number;
|
|
59
61
|
}
|
|
60
62
|
export interface ProxyTickReport {
|
|
61
63
|
outbound: OutboundResult;
|
|
@@ -118,6 +120,7 @@ export declare class ProxyDaemon {
|
|
|
118
120
|
private readonly reuseResultReporter;
|
|
119
121
|
private readonly validator;
|
|
120
122
|
private readonly atp;
|
|
123
|
+
private readonly collaborationFacade;
|
|
121
124
|
private ipc;
|
|
122
125
|
private readonly now;
|
|
123
126
|
private readonly random;
|
|
@@ -6,6 +6,7 @@ import { executeForceUpdate } from '../selfUpdate/executor.js';
|
|
|
6
6
|
import { reportPendingSelfUpdateLastUpdate, reportSelfUpdateLastUpdate } from '../selfUpdate/lastUpdate.js';
|
|
7
7
|
import { backfillProxyTraceUploads } from '../llm/traceBackfill.js';
|
|
8
8
|
import { hubAuthFailureHint } from './selectHub.js';
|
|
9
|
+
import { CollaborationFacade } from './collaborationFacade.js';
|
|
9
10
|
export const DEFAULT_IPC_PORT = 19820;
|
|
10
11
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
11
12
|
const MAX_PROXY_TICK_ERROR_LENGTH = 2_000;
|
|
@@ -28,6 +29,7 @@ export class ProxyDaemon {
|
|
|
28
29
|
reuseResultReporter;
|
|
29
30
|
validator;
|
|
30
31
|
atp;
|
|
32
|
+
collaborationFacade;
|
|
31
33
|
ipc;
|
|
32
34
|
now;
|
|
33
35
|
random;
|
|
@@ -93,9 +95,20 @@ export class ProxyDaemon {
|
|
|
93
95
|
pumpHandlers: ['core'], // proxy 出站归 SyncEngine, 不在此双 claim
|
|
94
96
|
...(deps.lockPath ? { lockPath: deps.lockPath } : {}),
|
|
95
97
|
});
|
|
98
|
+
this.collaborationFacade = new CollaborationFacade({
|
|
99
|
+
store: this.store,
|
|
100
|
+
hub: hubToUse,
|
|
101
|
+
now: this.now,
|
|
102
|
+
notifyOutbound: () => this.notifyNewOutbound(),
|
|
103
|
+
...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
|
|
104
|
+
...(deps.collaborationOperationTimeoutMs !== undefined ? { operationTimeoutMs: deps.collaborationOperationTimeoutMs } : {}),
|
|
105
|
+
});
|
|
96
106
|
this.sync = new SyncEngine({
|
|
97
107
|
store: this.store, hub: hubToUse, proxyHandler, now: this.now,
|
|
98
108
|
...(deps.runtimeNamespace ? { runtimeNamespace: deps.runtimeNamespace } : {}),
|
|
109
|
+
onOutboundSucceeded: (envelope, result) => this.collaborationFacade.handleOutboundSucceeded(envelope, result),
|
|
110
|
+
onOutboundTerminal: (envelope, error) => this.collaborationFacade.handleOutboundTerminal(envelope, error),
|
|
111
|
+
normalizeInboundEnvelope: (envelope) => this.collaborationFacade.normalizeInboundEnvelope(envelope),
|
|
99
112
|
...(deps.traceBackfill ? { onOutboundFlushed: () => { this.drainProxyTraceBackfill(); } } : {}),
|
|
100
113
|
});
|
|
101
114
|
this.lifecycle = new LifecycleManager({
|
|
@@ -555,6 +568,8 @@ export class ProxyDaemon {
|
|
|
555
568
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
556
569
|
}
|
|
557
570
|
async handleProxyRoute(ctx) {
|
|
571
|
+
if (await this.collaborationFacade.handle(ctx))
|
|
572
|
+
return true;
|
|
558
573
|
const handledAtp = await this.handleAtpRoute(ctx);
|
|
559
574
|
if (handledAtp)
|
|
560
575
|
return true;
|
|
@@ -711,6 +726,60 @@ export class ProxyDaemon {
|
|
|
711
726
|
ctx.json(200, { ...distill, queued: submission !== null, submission });
|
|
712
727
|
return true;
|
|
713
728
|
}
|
|
729
|
+
if (ctx.route === 'POST /agent/search') {
|
|
730
|
+
const body = asRecord(await ctx.readJson());
|
|
731
|
+
const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
|
|
732
|
+
const parsed = parseAgentSearchRequest(body);
|
|
733
|
+
if (!parsed.ok) {
|
|
734
|
+
respondAgentDirectory(ctx, parsed);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
const result = await directory.search(parsed.value);
|
|
738
|
+
respondAgentDirectory(ctx, result);
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
if (ctx.route === 'POST /agent/profile') {
|
|
742
|
+
const body = asRecord(await ctx.readJson());
|
|
743
|
+
const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
|
|
744
|
+
let agentId;
|
|
745
|
+
let timeoutMs;
|
|
746
|
+
try {
|
|
747
|
+
agentId = hubNs.normalizeAgentId(typeof body['agent_id'] === 'string' ? body['agent_id'] : '');
|
|
748
|
+
timeoutMs = hubNs.normalizeAgentDirectoryTimeout(typeof body['timeout_ms'] === 'number' ? body['timeout_ms'] : undefined);
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
respondAgentDirectory(ctx, invalidAgentDirectoryRequest(error));
|
|
752
|
+
return true;
|
|
753
|
+
}
|
|
754
|
+
const result = await directory.getProfile(agentId, { timeoutMs });
|
|
755
|
+
respondAgentDirectory(ctx, result);
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
if (ctx.route === 'POST /agent/discover') {
|
|
759
|
+
const body = asRecord(await ctx.readJson());
|
|
760
|
+
const directory = this.deps.hub.agentDirectory ?? hubNs.unsupportedAgentDirectoryCapability();
|
|
761
|
+
let request;
|
|
762
|
+
try {
|
|
763
|
+
request = hubNs.normalizeAgentTaskDiscoveryRequest({
|
|
764
|
+
title: typeof body['title'] === 'string' ? body['title'] : '',
|
|
765
|
+
...(typeof body['description'] === 'string' ? { description: body['description'] } : {}),
|
|
766
|
+
...(Array.isArray(body['signals']) ? { signals: body['signals'] } : {}),
|
|
767
|
+
...(typeof body['availability'] === 'string' ? { availability: body['availability'] } : {}),
|
|
768
|
+
...(typeof body['sort'] === 'string' ? { sort: body['sort'] } : {}),
|
|
769
|
+
...(typeof body['order'] === 'string' ? { order: body['order'] } : {}),
|
|
770
|
+
...(typeof body['cursor'] === 'string' ? { cursor: body['cursor'] } : {}),
|
|
771
|
+
...(typeof body['limit'] === 'number' ? { limit: body['limit'] } : {}),
|
|
772
|
+
...(typeof body['timeout_ms'] === 'number' ? { timeoutMs: body['timeout_ms'] } : {}),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
catch (error) {
|
|
776
|
+
respondAgentDirectory(ctx, invalidAgentDirectoryRequest(error));
|
|
777
|
+
return true;
|
|
778
|
+
}
|
|
779
|
+
const result = await directory.discoverForTask(request);
|
|
780
|
+
respondAgentDirectory(ctx, result);
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
714
783
|
}
|
|
715
784
|
async searchAssets(query) {
|
|
716
785
|
const limit = Math.max(1, Math.min(Number(query.limit ?? 5), 25));
|
|
@@ -862,6 +931,48 @@ function assetKind(value) {
|
|
|
862
931
|
function asRecord(value) {
|
|
863
932
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
864
933
|
}
|
|
934
|
+
function respondAgentDirectory(ctx, result) {
|
|
935
|
+
if (result.ok) {
|
|
936
|
+
ctx.json(200, result);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const status = {
|
|
940
|
+
invalid_request: 400,
|
|
941
|
+
permission_denied: 403,
|
|
942
|
+
capability_unavailable: 501,
|
|
943
|
+
invalid_response: 502,
|
|
944
|
+
hub_unavailable: 503,
|
|
945
|
+
timeout: 504,
|
|
946
|
+
}[result.error.code];
|
|
947
|
+
ctx.json(status, result);
|
|
948
|
+
}
|
|
949
|
+
function parseAgentSearchRequest(body) {
|
|
950
|
+
try {
|
|
951
|
+
return { ok: true, value: hubNs.normalizeAgentSearchRequest({
|
|
952
|
+
...(typeof body['query'] === 'string' ? { query: body['query'] } : {}),
|
|
953
|
+
...(Array.isArray(body['signals']) ? { signals: body['signals'] } : {}),
|
|
954
|
+
...(typeof body['availability'] === 'string' ? { availability: body['availability'] } : {}),
|
|
955
|
+
...(typeof body['sort'] === 'string' ? { sort: body['sort'] } : {}),
|
|
956
|
+
...(typeof body['order'] === 'string' ? { order: body['order'] } : {}),
|
|
957
|
+
...(typeof body['cursor'] === 'string' ? { cursor: body['cursor'] } : {}),
|
|
958
|
+
...(typeof body['limit'] === 'number' ? { limit: body['limit'] } : {}),
|
|
959
|
+
...(typeof body['timeout_ms'] === 'number' ? { timeoutMs: body['timeout_ms'] } : {}),
|
|
960
|
+
}) };
|
|
961
|
+
}
|
|
962
|
+
catch (error) {
|
|
963
|
+
return invalidAgentDirectoryRequest(error);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
function invalidAgentDirectoryRequest(error) {
|
|
967
|
+
return {
|
|
968
|
+
ok: false,
|
|
969
|
+
error: {
|
|
970
|
+
code: 'invalid_request',
|
|
971
|
+
retryable: false,
|
|
972
|
+
message: error instanceof Error ? error.message.slice(0, 120) : 'invalid_request',
|
|
973
|
+
},
|
|
974
|
+
};
|
|
975
|
+
}
|
|
865
976
|
function stringBody(body, key) {
|
|
866
977
|
const v = body[key];
|
|
867
978
|
return typeof v === 'string' && v.length > 0 ? v : undefined;
|
package/dist/daemon/selectHub.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveHubUrl as resolvePublicHubUrl } from '@evomap/evolver-adapter-public';
|
|
1
2
|
/** 据 EVOMAP_HUB_MODE 选 hub 实现(public|private). 缺省 public. bin 据此挂对应 adapter. */
|
|
2
3
|
export function resolveHubMode(env) {
|
|
3
4
|
const m = (env['EVOMAP_HUB_MODE'] ?? 'public').toLowerCase();
|
|
@@ -6,7 +7,22 @@ export function resolveHubMode(env) {
|
|
|
6
7
|
return m;
|
|
7
8
|
}
|
|
8
9
|
export function resolveHubUrl(env) {
|
|
9
|
-
|
|
10
|
+
if ((env['EVOMAP_HUB_MODE'] ?? 'public').toLowerCase() === 'private')
|
|
11
|
+
return resolvePrivateHubUrl(env);
|
|
12
|
+
return resolvePublicHubUrl(env);
|
|
13
|
+
}
|
|
14
|
+
function resolvePrivateHubUrl(env) {
|
|
15
|
+
return trimmed(env['EVOMAP_HUB_URL'])
|
|
16
|
+
?? trimmed(env['A2A_HUB_URL'])
|
|
17
|
+
?? trimmed(env['EVOLVER_DEFAULT_HUB_URL'])
|
|
18
|
+
?? resolvePublicHubUrl({});
|
|
19
|
+
}
|
|
20
|
+
function trimmed(value) {
|
|
21
|
+
const v = value?.trim();
|
|
22
|
+
if (!v)
|
|
23
|
+
return undefined;
|
|
24
|
+
const normalized = v.replace(/\/+$/, '');
|
|
25
|
+
return normalized || undefined;
|
|
10
26
|
}
|
|
11
27
|
/**
|
|
12
28
|
* Actionable hint for a hub AUTH failure (401/403), tailored to the hub's error code so it does NOT misdirect
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { hub as hubNs } from '@evomap/evolver-core';
|
|
2
2
|
import type { HelloResult, HeartbeatOptions, HeartbeatResult } from '../lifecycle/manager.js';
|
|
3
3
|
export type PrivateHubWithLifecycle = hubNs.HubCapability & {
|
|
4
4
|
hello(opts: {
|
|
@@ -27,6 +27,8 @@ export interface ConnectPrivateHubOptions {
|
|
|
27
27
|
senderId: () => string | undefined;
|
|
28
28
|
env?: Record<string, string | undefined>;
|
|
29
29
|
now?: () => number;
|
|
30
|
+
/** One-shot invitation token (evoinv_…) — preferred over the SSO bearer for token_required hubs. */
|
|
31
|
+
invitationToken?: string;
|
|
30
32
|
}
|
|
31
33
|
type DynamicImporter = (specifier: string) => Promise<unknown>;
|
|
32
34
|
export interface PrivateProxyHubRuntime {
|
|
@@ -41,6 +43,9 @@ export interface ConnectPrivateProxyHubOptions {
|
|
|
41
43
|
importer?: DynamicImporter;
|
|
42
44
|
}
|
|
43
45
|
export declare function resolvePrivateEnterpriseToken(env: Record<string, string | undefined>): string | undefined;
|
|
46
|
+
/** One-shot invitation token (evoinv_…), matching the hub's official onboarding script (A2A_INVITATION_TOKEN).
|
|
47
|
+
* Preferred over the enterprise token for the default token_required enrollment mode. */
|
|
48
|
+
export declare function resolvePrivateInvitationToken(env: Record<string, string | undefined>): string | undefined;
|
|
44
49
|
export declare function resolvePrivateEnterpriseSubject(env: Record<string, string | undefined>): string;
|
|
45
50
|
export declare function connectPrivateProxyHub(opts: ConnectPrivateProxyHubOptions): Promise<PrivateProxyHubRuntime>;
|
|
46
51
|
export {};
|
|
@@ -1,14 +1,21 @@
|
|
|
1
|
+
import { hub as hubNs } from '@evomap/evolver-core';
|
|
1
2
|
const DEFAULT_PRIVATE_ADAPTER_MODULE = '@evomap/evolver-adapter-private';
|
|
2
3
|
export function resolvePrivateEnterpriseToken(env) {
|
|
3
4
|
return firstEnv(env, 'EVOMAP_ENTERPRISE_TOKEN', 'EVOMAP_PRIVATE_HUB_TOKEN', 'PHUB_ENTERPRISE_TOKEN', 'PRIVATE_HUB_ENTERPRISE_TOKEN');
|
|
4
5
|
}
|
|
6
|
+
/** One-shot invitation token (evoinv_…), matching the hub's official onboarding script (A2A_INVITATION_TOKEN).
|
|
7
|
+
* Preferred over the enterprise token for the default token_required enrollment mode. */
|
|
8
|
+
export function resolvePrivateInvitationToken(env) {
|
|
9
|
+
return firstEnv(env, 'A2A_INVITATION_TOKEN');
|
|
10
|
+
}
|
|
5
11
|
export function resolvePrivateEnterpriseSubject(env) {
|
|
6
12
|
return firstEnv(env, 'EVOMAP_ENTERPRISE_SUBJECT', 'EVOMAP_PRIVATE_SUBJECT', 'PHUB_ENTERPRISE_SUBJECT', 'USER') ?? 'evolver-proxy';
|
|
7
13
|
}
|
|
8
14
|
export async function connectPrivateProxyHub(opts) {
|
|
15
|
+
const invitationToken = resolvePrivateInvitationToken(opts.env);
|
|
9
16
|
const token = resolvePrivateEnterpriseToken(opts.env);
|
|
10
|
-
if (!token) {
|
|
11
|
-
throw new Error('EVOMAP_HUB_MODE=private 需要 EVOMAP_ENTERPRISE_TOKEN(也兼容 EVOMAP_PRIVATE_HUB_TOKEN / PHUB_ENTERPRISE_TOKEN)');
|
|
17
|
+
if (!token && !invitationToken) {
|
|
18
|
+
throw new Error('EVOMAP_HUB_MODE=private 需要 A2A_INVITATION_TOKEN(推荐,对齐 PrivateHub onboarding)或 EVOMAP_ENTERPRISE_TOKEN(也兼容 EVOMAP_PRIVATE_HUB_TOKEN / PHUB_ENTERPRISE_TOKEN)');
|
|
12
19
|
}
|
|
13
20
|
const moduleName = opts.env['EVOMAP_PRIVATE_ADAPTER_MODULE']?.trim() || DEFAULT_PRIVATE_ADAPTER_MODULE;
|
|
14
21
|
const connectPrivateHub = await loadConnectPrivateHub(moduleName, opts.importer ?? ((specifier) => import(specifier)));
|
|
@@ -21,11 +28,15 @@ export async function connectPrivateProxyHub(opts) {
|
|
|
21
28
|
now,
|
|
22
29
|
sso: {
|
|
23
30
|
identity: () => ({ subject }),
|
|
24
|
-
exchange: async () => ({ token }),
|
|
31
|
+
exchange: async () => ({ token: token ?? '' }),
|
|
25
32
|
now,
|
|
26
33
|
},
|
|
34
|
+
...(invitationToken ? { invitationToken } : {}),
|
|
27
35
|
});
|
|
28
36
|
assertPrivateLifecycle(hub, moduleName);
|
|
37
|
+
if (!hub.agentDirectory) {
|
|
38
|
+
hub.agentDirectory = hubNs.unsupportedAgentDirectoryCapability('private_hub_agent_directory_not_supported');
|
|
39
|
+
}
|
|
29
40
|
return { hub, auth };
|
|
30
41
|
}
|
|
31
42
|
async function loadConnectPrivateHub(moduleName, importer) {
|
|
@@ -112,6 +112,19 @@ export interface MessagesResponse {
|
|
|
112
112
|
}
|
|
113
113
|
export declare function captureTraceMetadata(value: unknown, env?: NodeJS.ProcessEnv): unknown;
|
|
114
114
|
export declare function resolveTierModels(env?: NodeJS.ProcessEnv): Partial<Record<Tier, string>>;
|
|
115
|
+
export type TierConfigWarningReason = 'missing_tier_models' | 'duplicate_tier_models' | 'all_tier_models_same';
|
|
116
|
+
export interface TierConfigWarning {
|
|
117
|
+
event: 'router_config_warning';
|
|
118
|
+
reason: TierConfigWarningReason;
|
|
119
|
+
message: string;
|
|
120
|
+
configured_tiers: Tier[];
|
|
121
|
+
missing_tiers?: Tier[];
|
|
122
|
+
duplicate_models?: Array<{
|
|
123
|
+
model: string;
|
|
124
|
+
tiers: Tier[];
|
|
125
|
+
}>;
|
|
126
|
+
}
|
|
127
|
+
export declare function detectTierModelConfigWarnings(models: Partial<Record<Tier, string>>): TierConfigWarning[];
|
|
115
128
|
interface ClaudeId {
|
|
116
129
|
family: string;
|
|
117
130
|
major: number;
|
|
@@ -34,6 +34,58 @@ export function resolveTierModels(env = process.env) {
|
|
|
34
34
|
out.expensive = env['EVOMAP_MODEL_EXPENSIVE'];
|
|
35
35
|
return out;
|
|
36
36
|
}
|
|
37
|
+
const TIER_ORDER = ['cheap', 'mid', 'expensive'];
|
|
38
|
+
export function detectTierModelConfigWarnings(models) {
|
|
39
|
+
const configuredTiers = TIER_ORDER.filter((tier) => {
|
|
40
|
+
const model = models[tier];
|
|
41
|
+
return typeof model === 'string' && model.length > 0;
|
|
42
|
+
});
|
|
43
|
+
if (configuredTiers.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const warnings = [];
|
|
46
|
+
const missingTiers = TIER_ORDER.filter((tier) => !configuredTiers.includes(tier));
|
|
47
|
+
if (missingTiers.length > 0) {
|
|
48
|
+
warnings.push({
|
|
49
|
+
event: 'router_config_warning',
|
|
50
|
+
reason: 'missing_tier_models',
|
|
51
|
+
message: 'Router tier config is partial; unset tiers will pass the client model through.',
|
|
52
|
+
configured_tiers: configuredTiers,
|
|
53
|
+
missing_tiers: missingTiers,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const byModel = new Map();
|
|
57
|
+
for (const tier of configuredTiers) {
|
|
58
|
+
const model = models[tier];
|
|
59
|
+
if (!model)
|
|
60
|
+
continue;
|
|
61
|
+
byModel.set(model, [...(byModel.get(model) ?? []), tier]);
|
|
62
|
+
}
|
|
63
|
+
const duplicateModels = Array.from(byModel.entries())
|
|
64
|
+
.filter(([, tiers]) => tiers.length > 1)
|
|
65
|
+
.map(([model, tiers]) => ({ model, tiers }));
|
|
66
|
+
const allSame = configuredTiers.length === TIER_ORDER.length
|
|
67
|
+
&& duplicateModels.length === 1
|
|
68
|
+
&& duplicateModels[0].tiers.length === TIER_ORDER.length;
|
|
69
|
+
if (allSame) {
|
|
70
|
+
warnings.push({
|
|
71
|
+
event: 'router_config_warning',
|
|
72
|
+
reason: 'all_tier_models_same',
|
|
73
|
+
message: 'All router tiers resolve to the same model; routing will still run but cannot change cost/latency tiers.',
|
|
74
|
+
configured_tiers: configuredTiers,
|
|
75
|
+
duplicate_models: duplicateModels,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else if (duplicateModels.length > 0) {
|
|
79
|
+
warnings.push({
|
|
80
|
+
event: 'router_config_warning',
|
|
81
|
+
reason: 'duplicate_tier_models',
|
|
82
|
+
message: 'Multiple router tiers resolve to the same model; routing will still run but tier separation is degraded.',
|
|
83
|
+
configured_tiers: configuredTiers,
|
|
84
|
+
duplicate_models: duplicateModels,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return warnings;
|
|
88
|
+
}
|
|
37
89
|
export function parseClaudeId(modelId) {
|
|
38
90
|
if (typeof modelId !== 'string')
|
|
39
91
|
return null;
|
|
@@ -386,6 +438,10 @@ export function buildMessagesHandler(opts) {
|
|
|
386
438
|
const log = opts.logger ?? console;
|
|
387
439
|
const env = opts.env ?? process.env;
|
|
388
440
|
const enabled = typeof opts.routerEnabled === 'boolean' ? opts.routerEnabled : env['EVOMAP_ROUTER_ENABLED'] === '1';
|
|
441
|
+
if (enabled) {
|
|
442
|
+
for (const warning of detectTierModelConfigWarnings(resolveTierModels(env)))
|
|
443
|
+
log.warn?.(j(warning));
|
|
444
|
+
}
|
|
389
445
|
// Native body capture defaults to v1-compatible full trace mode. Operators that need metadata-only rows must
|
|
390
446
|
// explicitly disable it (a compliance decision; see bodyCapture.ts and docs/trace-body-capture.md).
|
|
391
447
|
const captureBodies = captureBodiesEnabled(env);
|
package/dist/sync/engine.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface SyncEngineDeps {
|
|
|
19
19
|
now: () => number;
|
|
20
20
|
runtimeNamespace?: string;
|
|
21
21
|
leaseMs?: number;
|
|
22
|
+
/** Finalize durable facade state after the Hub accepted one outbound envelope. */
|
|
23
|
+
onOutboundSucceeded?: (envelope: Envelope, result: unknown) => void | Promise<void>;
|
|
24
|
+
/** Cache a durable facade terminal result after SyncEngine moved the envelope to DLQ. */
|
|
25
|
+
onOutboundTerminal?: (envelope: Envelope, error: unknown) => void | Promise<void>;
|
|
26
|
+
/** Canonicalize an inbound envelope before durable insertion. */
|
|
27
|
+
normalizeInboundEnvelope?: (envelope: Envelope) => Envelope;
|
|
22
28
|
onOutboundFlushed?: (result: OutboundResult) => void | Promise<void>;
|
|
23
29
|
env?: NodeJS.ProcessEnv;
|
|
24
30
|
}
|
|
@@ -36,11 +42,6 @@ export interface InboundResult {
|
|
|
36
42
|
nextPollAfterMs?: number;
|
|
37
43
|
hasMore: boolean;
|
|
38
44
|
}
|
|
39
|
-
/**
|
|
40
|
-
* SyncEngine(M6-2): proxy↔hub 双向同步. 移植 v1 sync/{engine,outbound,inbound} 到 TS,
|
|
41
|
-
* hub I/O 全走 HubCapability.mailbox(非裸 hubFetch). tick 方法纯逻辑(注入 now), 可对 FakeHubCapability 确定性测;
|
|
42
|
-
* 定时器循环(start/stop)是薄包装, 由 M6-4 daemon 装配驱动.
|
|
43
|
-
*/
|
|
44
45
|
export declare class SyncEngine {
|
|
45
46
|
private readonly deps;
|
|
46
47
|
private lastActivityAt;
|
package/dist/sync/engine.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mailbox, hub as hubNs } from '@evomap/evolver-core';
|
|
2
|
+
import { HubClientError } from '@evomap/evolver-adapter-public';
|
|
2
3
|
import { normalizeProxyTraceOutboundPayload } from '../llm/traceBackfill.js';
|
|
3
4
|
import { applyTraceCollectionConfig } from '../llm/traceControl.js';
|
|
4
5
|
import { hubAuthFailureHint } from '../daemon/selectHub.js';
|
|
@@ -15,8 +16,12 @@ const STATE = {
|
|
|
15
16
|
lastError: 'sync:last_error',
|
|
16
17
|
authStatus: 'hub:auth_status',
|
|
17
18
|
};
|
|
18
|
-
function isTerminal(err) {
|
|
19
|
-
|
|
19
|
+
function isTerminal(err, envelopeType) {
|
|
20
|
+
if (err instanceof hubNs.PublishRejectedError && err.terminal === true)
|
|
21
|
+
return true;
|
|
22
|
+
return (envelopeType === 'task_claim' || envelopeType === 'task_complete')
|
|
23
|
+
&& err instanceof HubClientError
|
|
24
|
+
&& (err.status === 404 || err.status === 409);
|
|
20
25
|
}
|
|
21
26
|
function isRetryableRejection(err) {
|
|
22
27
|
return err instanceof hubNs.PublishRejectedError
|
|
@@ -27,9 +32,30 @@ function isHubUnreachable(err) {
|
|
|
27
32
|
const e = err;
|
|
28
33
|
return e?.name === 'HubUnreachableError' || e?.code === 'HUB_UNREACHABLE';
|
|
29
34
|
}
|
|
35
|
+
function isRetryableTransportError(err) {
|
|
36
|
+
if (isRetryableRejection(err) || isHubUnreachable(err)
|
|
37
|
+
|| (err instanceof HubClientError && err.status === 429))
|
|
38
|
+
return true;
|
|
39
|
+
const e = err;
|
|
40
|
+
const rawStatus = e?.statusCode ?? e?.status;
|
|
41
|
+
const status = typeof rawStatus === 'number' ? rawStatus : Number(rawStatus);
|
|
42
|
+
if (Number.isFinite(status) && status >= 500 && status <= 599)
|
|
43
|
+
return true;
|
|
44
|
+
if (e?.retryable === true)
|
|
45
|
+
return true;
|
|
46
|
+
const signature = `${String(e?.name ?? '')} ${String(e?.code ?? '')} ${err instanceof Error ? err.message : ''}`;
|
|
47
|
+
return /\b5\d\d\b|ECONN|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|fetch/i.test(signature);
|
|
48
|
+
}
|
|
30
49
|
function retryAfterMs(err) {
|
|
50
|
+
const body = err instanceof HubClientError && err.body && typeof err.body === 'object' && !Array.isArray(err.body)
|
|
51
|
+
? err.body
|
|
52
|
+
: undefined;
|
|
53
|
+
const bodyRetryMs = Number(body?.['retry_after_ms'] ?? body?.['retryAfterMs']);
|
|
54
|
+
const bodyRetrySeconds = Number(body?.['retry_after'] ?? body?.['retryAfter']);
|
|
31
55
|
const retry = err?.retryAfterMs
|
|
32
|
-
?? err?.details?.retryAfterMs
|
|
56
|
+
?? err?.details?.retryAfterMs
|
|
57
|
+
?? (Number.isFinite(bodyRetryMs) ? bodyRetryMs : undefined)
|
|
58
|
+
?? (Number.isFinite(bodyRetrySeconds) ? bodyRetrySeconds * 1_000 : undefined);
|
|
33
59
|
return Math.max(1_000, typeof retry === 'number' && Number.isFinite(retry) ? retry : 60_000);
|
|
34
60
|
}
|
|
35
61
|
function errorMessage(err) {
|
|
@@ -74,6 +100,53 @@ function envelopeToAgentEvent(e) {
|
|
|
74
100
|
* hub I/O 全走 HubCapability.mailbox(非裸 hubFetch). tick 方法纯逻辑(注入 now), 可对 FakeHubCapability 确定性测;
|
|
75
101
|
* 定时器循环(start/stop)是薄包装, 由 M6-4 daemon 装配驱动.
|
|
76
102
|
*/
|
|
103
|
+
function applyMailboxPushManyOutcomes(group, outcomes, deps) {
|
|
104
|
+
const byId = new Map(outcomes.map((outcome) => [outcome.id, outcome]));
|
|
105
|
+
const result = { sent: 0, failed: 0, terminal: 0, deferred: 0, completedDuplicates: 0 };
|
|
106
|
+
for (const pushed of group) {
|
|
107
|
+
const outcome = byId.get(pushed.event.id) ?? byId.get(pushed.original.id);
|
|
108
|
+
if (outcome?.status === 'failed') {
|
|
109
|
+
const msg = redactAndTruncate(outcome.reason ?? 'mailbox_push_rejected');
|
|
110
|
+
const authLike = isAuthError(msg);
|
|
111
|
+
result.firstFailure ??= msg;
|
|
112
|
+
const retryable = outcome.terminal !== true
|
|
113
|
+
&& (outcome.retryable === true || (typeof outcome.retryAfterMs === 'number' && Number.isFinite(outcome.retryAfterMs)));
|
|
114
|
+
if (authLike || retryable) {
|
|
115
|
+
const nowAfterFailure = deps.now();
|
|
116
|
+
const retry = Math.max(1_000, typeof outcome.retryAfterMs === 'number' && Number.isFinite(outcome.retryAfterMs) ? outcome.retryAfterMs : 60_000);
|
|
117
|
+
result.deferredFailure ??= { msg, retryAfterMs: retry };
|
|
118
|
+
if (authLike) {
|
|
119
|
+
result.authFailed = true;
|
|
120
|
+
result.authErrorMessage ??= msg;
|
|
121
|
+
}
|
|
122
|
+
deps.store.defer(pushed.original.id, msg, nowAfterFailure, retry);
|
|
123
|
+
for (const duplicate of pushed.duplicates)
|
|
124
|
+
deps.store.defer(duplicate.id, msg, nowAfterFailure, retry);
|
|
125
|
+
result.deferred += 1 + pushed.duplicates.length;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
const maxAttempts = outcome.terminal ? 1 : undefined;
|
|
129
|
+
deps.store.fail(pushed.original.id, msg, deps.now(), maxAttempts);
|
|
130
|
+
for (const duplicate of pushed.duplicates)
|
|
131
|
+
deps.store.fail(duplicate.id, msg, deps.now(), maxAttempts);
|
|
132
|
+
if (outcome.terminal)
|
|
133
|
+
result.terminal += 1 + pushed.duplicates.length;
|
|
134
|
+
else
|
|
135
|
+
result.failed += 1 + pushed.duplicates.length;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
deps.store.complete(pushed.original.id, deps.now());
|
|
140
|
+
for (const duplicate of pushed.duplicates)
|
|
141
|
+
deps.store.complete(duplicate.id, deps.now());
|
|
142
|
+
if (pushed.dedupKey)
|
|
143
|
+
deps.store.markProcessed(pushed.dedupKey, { type: pushed.original.type }, deps.now());
|
|
144
|
+
result.sent += 1;
|
|
145
|
+
result.completedDuplicates += pushed.duplicates.length;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
77
150
|
export class SyncEngine {
|
|
78
151
|
deps;
|
|
79
152
|
lastActivityAt;
|
|
@@ -133,57 +206,21 @@ export class SyncEngine {
|
|
|
133
206
|
try {
|
|
134
207
|
const result = await this.deps.hub.mailbox.pushMany(group.map((entry) => entry.event));
|
|
135
208
|
if (isMailboxPushManyResult(result)) {
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const nowAfterFailure = this.deps.now();
|
|
149
|
-
const retry = Math.max(1_000, typeof outcome.retryAfterMs === 'number' && Number.isFinite(outcome.retryAfterMs) ? outcome.retryAfterMs : 60_000);
|
|
150
|
-
deferredFailure ??= { msg, retryAfterMs: retry };
|
|
151
|
-
if (authLike) {
|
|
152
|
-
authFailed = true;
|
|
153
|
-
authErrorMessage ??= msg;
|
|
154
|
-
}
|
|
155
|
-
this.deps.store.defer(pushed.original.id, msg, nowAfterFailure, retry);
|
|
156
|
-
for (const duplicate of pushed.duplicates)
|
|
157
|
-
this.deps.store.defer(duplicate.id, msg, nowAfterFailure, retry);
|
|
158
|
-
deferred += 1 + pushed.duplicates.length;
|
|
159
|
-
}
|
|
160
|
-
else {
|
|
161
|
-
const maxAttempts = outcome.terminal ? 1 : undefined;
|
|
162
|
-
this.deps.store.fail(pushed.original.id, msg, this.deps.now(), maxAttempts);
|
|
163
|
-
for (const duplicate of pushed.duplicates)
|
|
164
|
-
this.deps.store.fail(duplicate.id, msg, this.deps.now(), maxAttempts);
|
|
165
|
-
if (outcome.terminal)
|
|
166
|
-
terminal += 1 + pushed.duplicates.length;
|
|
167
|
-
else
|
|
168
|
-
failed += 1 + pushed.duplicates.length;
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
else {
|
|
172
|
-
this.deps.store.complete(pushed.original.id, this.deps.now());
|
|
173
|
-
for (const duplicate of pushed.duplicates)
|
|
174
|
-
this.deps.store.complete(duplicate.id, this.deps.now());
|
|
175
|
-
if (pushed.dedupKey)
|
|
176
|
-
this.deps.store.markProcessed(pushed.dedupKey, { type: pushed.original.type }, this.deps.now());
|
|
177
|
-
sent += 1;
|
|
178
|
-
completedDuplicates += pushed.duplicates.length;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
if (firstFailure)
|
|
182
|
-
this.markError(`outbound: ${firstFailure}`, isAuthError(firstFailure));
|
|
183
|
-
if (deferredFailure) {
|
|
209
|
+
const applied = applyMailboxPushManyOutcomes(group, result.outcomes, this.deps);
|
|
210
|
+
sent += applied.sent;
|
|
211
|
+
failed += applied.failed;
|
|
212
|
+
terminal += applied.terminal;
|
|
213
|
+
deferred += applied.deferred;
|
|
214
|
+
completedDuplicates += applied.completedDuplicates;
|
|
215
|
+
if (applied.authFailed)
|
|
216
|
+
authFailed = true;
|
|
217
|
+
authErrorMessage ??= applied.authErrorMessage;
|
|
218
|
+
if (applied.firstFailure)
|
|
219
|
+
this.markError(`outbound: ${applied.firstFailure}`, isAuthError(applied.firstFailure));
|
|
220
|
+
if (applied.deferredFailure) {
|
|
184
221
|
const nowAfterFailure = this.deps.now();
|
|
185
222
|
for (const pending of batch.slice(j)) {
|
|
186
|
-
this.deps.store.defer(pending.id, deferredFailure.msg, nowAfterFailure, deferredFailure.retryAfterMs);
|
|
223
|
+
this.deps.store.defer(pending.id, applied.deferredFailure.msg, nowAfterFailure, applied.deferredFailure.retryAfterMs);
|
|
187
224
|
deferred += 1;
|
|
188
225
|
}
|
|
189
226
|
break;
|
|
@@ -224,7 +261,7 @@ export class SyncEngine {
|
|
|
224
261
|
authErrorMessage ??= msg;
|
|
225
262
|
break;
|
|
226
263
|
}
|
|
227
|
-
else if (isTerminal(err)) {
|
|
264
|
+
else if (isTerminal(err, group[0].original.type)) {
|
|
228
265
|
for (const pushed of group) {
|
|
229
266
|
this.deps.store.fail(pushed.original.id, msg, this.deps.now(), 1);
|
|
230
267
|
for (const duplicate of pushed.duplicates)
|
|
@@ -233,7 +270,7 @@ export class SyncEngine {
|
|
|
233
270
|
}
|
|
234
271
|
i = j - 1;
|
|
235
272
|
}
|
|
236
|
-
else if (
|
|
273
|
+
else if (isRetryableTransportError(err)) {
|
|
237
274
|
const nowAfterFailure = this.deps.now();
|
|
238
275
|
const retry = retryAfterMs(err);
|
|
239
276
|
for (const pushed of group) {
|
|
@@ -273,7 +310,8 @@ export class SyncEngine {
|
|
|
273
310
|
continue;
|
|
274
311
|
}
|
|
275
312
|
try {
|
|
276
|
-
await this.deps.proxyHandler(guard.envelope);
|
|
313
|
+
const handlerResult = await this.deps.proxyHandler(guard.envelope);
|
|
314
|
+
await this.deps.onOutboundSucceeded?.(e, handlerResult);
|
|
277
315
|
this.deps.store.complete(e.id, this.deps.now());
|
|
278
316
|
if (outboundDedupKey)
|
|
279
317
|
this.deps.store.markProcessed(outboundDedupKey, { type: e.type }, this.deps.now());
|
|
@@ -294,11 +332,15 @@ export class SyncEngine {
|
|
|
294
332
|
authErrorMessage ??= msg;
|
|
295
333
|
break;
|
|
296
334
|
}
|
|
297
|
-
else if (isTerminal(err)) {
|
|
335
|
+
else if (isTerminal(err, e.type)) {
|
|
336
|
+
// A concurrent facade attempt may already have finalized this durable intent successfully.
|
|
337
|
+
if (this.deps.store.getById(e.id)?.status === 'done')
|
|
338
|
+
continue;
|
|
298
339
|
this.deps.store.fail(e.id, msg, this.deps.now(), 1); // maxAttempts=1 → 直进 DLQ, 不反复打经济端点
|
|
340
|
+
await this.deps.onOutboundTerminal?.(e, err);
|
|
299
341
|
terminal += 1;
|
|
300
342
|
}
|
|
301
|
-
else if (
|
|
343
|
+
else if (isRetryableTransportError(err)) {
|
|
302
344
|
const nowAfterFailure = this.deps.now();
|
|
303
345
|
const retry = retryAfterMs(err);
|
|
304
346
|
for (const pending of batch.slice(i)) {
|
|
@@ -347,7 +389,8 @@ export class SyncEngine {
|
|
|
347
389
|
this.deps.store.setState(CURSOR_KEY, ev.cursor);
|
|
348
390
|
continue;
|
|
349
391
|
}
|
|
350
|
-
const
|
|
392
|
+
const rawEnvelope = this.toEnvelope(ev);
|
|
393
|
+
const env = rawEnvelope ? (this.deps.normalizeInboundEnvelope?.(rawEnvelope) ?? rawEnvelope) : null;
|
|
351
394
|
if (env) {
|
|
352
395
|
this.deps.store.send(env);
|
|
353
396
|
this.deps.store.markProcessed(dedupKey, { type: ev.type }, this.deps.now());
|
|
@@ -450,6 +493,7 @@ export class SyncEngine {
|
|
|
450
493
|
try {
|
|
451
494
|
return mailbox.createEnvelope({
|
|
452
495
|
type: ev.type, payload: ev.payload, idempotencyKey: `inbound:${ev.id}`,
|
|
496
|
+
...(ev.refId ? { replyTo: ev.refId } : {}),
|
|
453
497
|
...(this.deps.runtimeNamespace ? { runtimeNamespace: this.deps.runtimeNamespace } : {}),
|
|
454
498
|
now: this.deps.now(),
|
|
455
499
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-proxy",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "系统级 mailbox/hub 同步 daemon (Node)",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@aws-sdk/client-bedrock-runtime": "^3.1053.0",
|
|
29
|
-
"@evomap/evolver-adapter-public": "2.0.0-beta.
|
|
30
|
-
"@evomap/evolver-core": "2.0.0-beta.
|
|
29
|
+
"@evomap/evolver-adapter-public": "2.0.0-beta.2",
|
|
30
|
+
"@evomap/evolver-core": "2.0.0-beta.2"
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public",
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"files": [
|
|
37
37
|
"dist/",
|
|
38
|
+
"assets/",
|
|
38
39
|
"README.md",
|
|
39
40
|
"package.json"
|
|
40
41
|
]
|