@evomap/evolver-adapter-public 2.0.0-beta.14 → 2.0.0-beta.16
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/hubCapability.d.ts +5 -1
- package/dist/hubCapability.js +152 -5
- package/dist/hubFetch.d.ts +1 -1
- package/dist/hubFetch.js +20 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/learningPacketSink.d.ts +34 -0
- package/dist/learningPacketSink.js +142 -0
- package/dist/wireMap.js +9 -2
- package/package.json +2 -2
package/dist/hubCapability.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const PUBLIC_PROTOCOL_VERSION = "gep-a2a/1.0.0";
|
|
|
8
8
|
export declare const PUBLIC_HUB_CAPABILITIES: hub.HubCapabilityName[];
|
|
9
9
|
export declare const USED_ASSET_IDS_MAX = 50;
|
|
10
10
|
export declare const USED_ASSET_ID_MAX_LEN = 200;
|
|
11
|
+
export declare const LEARNING_ASSET_IDS_MAX = 50;
|
|
12
|
+
export declare const LEARNING_ASSET_ID_MAX_LEN = 128;
|
|
11
13
|
/**
|
|
12
14
|
* An outcome the agent reports back to the hub's memory graph after a cycle.
|
|
13
15
|
* `usedAssetIds` is the fetch->outcome attribution claim: which hub assets the
|
|
@@ -159,6 +161,8 @@ export declare class PublicHubCapability implements hub.HubCapability {
|
|
|
159
161
|
recordOutcome(report: OutcomeReport): Promise<OutcomeReceipt>;
|
|
160
162
|
recordMemoryEvent(report: MemoryGraphEventReport): Promise<MemoryGraphEventReceipt>;
|
|
161
163
|
recordReuseResult(report: hub.ReuseResultReport): Promise<hub.ReuseResultReceipt>;
|
|
164
|
+
listLearningAssets(options?: hub.LearningAssetListOptions): Promise<hub.LearningAssetListResult>;
|
|
165
|
+
recordLearningAssetUsage(report: hub.LearningAssetUsageReport): Promise<hub.LearningAssetUsageReceipt>;
|
|
162
166
|
/**
|
|
163
167
|
* Pre-publish dry-run (POST /a2a/validate). The hub runs the same hub-side quality +
|
|
164
168
|
* content-safety gate as publish but stores nothing and charges no credits. This adapter is
|
|
@@ -171,7 +175,7 @@ export declare class PublicHubCapability implements hub.HubCapability {
|
|
|
171
175
|
*/
|
|
172
176
|
validate(bundle: hub.AssetRecord[]): Promise<hub.ValidateReceipt>;
|
|
173
177
|
createRecipe(request: hub.RecipeCreateRequest): Promise<hub.RecipeReceipt>;
|
|
174
|
-
publishRecipe(recipeId: string): Promise<hub.RecipeReceipt>;
|
|
178
|
+
publishRecipe(recipeId: string, options?: hub.RecipePublishOptions): Promise<hub.RecipeReceipt>;
|
|
175
179
|
getRecipe(recipeId: string): Promise<hub.RecipeFetchReceipt>;
|
|
176
180
|
expressRecipe(recipeId: string, request?: hub.RecipeExpressRequest): Promise<hub.RecipeExpressionReceipt>;
|
|
177
181
|
task: {
|
package/dist/hubCapability.js
CHANGED
|
@@ -9,7 +9,7 @@ export const INBOUND_LIMIT = 100;
|
|
|
9
9
|
export const OUTBOUND_MAX_BATCH = 50;
|
|
10
10
|
export const OUTBOUND_MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
11
11
|
export const PUBLIC_PROTOCOL_VERSION = 'gep-a2a/1.0.0';
|
|
12
|
-
export const PUBLIC_HUB_CAPABILITIES = ['publish', 'fetch', 'search', 'task', 'mailbox', 'auth', 'marketplace', 'economy', 'questions', 'recipes', 'agent_directory'];
|
|
12
|
+
export const PUBLIC_HUB_CAPABILITIES = ['publish', 'fetch', 'search', 'task', 'mailbox', 'auth', 'marketplace', 'economy', 'questions', 'recipes', 'agent_directory', 'learning_assets'];
|
|
13
13
|
const QUESTION_SUBMIT_FAST_PATH_BYPASS_CONTENT_HASH = 'sha256:0000000000000000000000000000000000000000000000000000000000000000';
|
|
14
14
|
const DRY_RUN_RECIPE_ID = 'dry-run-recipe';
|
|
15
15
|
const HUB_DRY_RUN_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
@@ -18,6 +18,8 @@ const HUB_DRY_RUN_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
|
18
18
|
// equals what the hub will KEEP.
|
|
19
19
|
export const USED_ASSET_IDS_MAX = 50;
|
|
20
20
|
export const USED_ASSET_ID_MAX_LEN = 200;
|
|
21
|
+
export const LEARNING_ASSET_IDS_MAX = 50;
|
|
22
|
+
export const LEARNING_ASSET_ID_MAX_LEN = 128;
|
|
21
23
|
/** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
|
|
22
24
|
export function gepEnvelope(messageType, payload) {
|
|
23
25
|
return {
|
|
@@ -83,7 +85,7 @@ export class PublicHubCapability {
|
|
|
83
85
|
auth;
|
|
84
86
|
recipes = {
|
|
85
87
|
create: async (request) => this.createRecipe(request),
|
|
86
|
-
publish: async (recipeId) => this.publishRecipe(recipeId),
|
|
88
|
+
publish: async (recipeId, options) => this.publishRecipe(recipeId, options),
|
|
87
89
|
get: async (recipeId) => this.getRecipe(recipeId),
|
|
88
90
|
express: async (recipeId, request = {}) => this.expressRecipe(recipeId, request),
|
|
89
91
|
};
|
|
@@ -402,6 +404,52 @@ export class PublicHubCapability {
|
|
|
402
404
|
return { recorded: false, reason: e instanceof Error ? e.message : String(e) };
|
|
403
405
|
}
|
|
404
406
|
}
|
|
407
|
+
async listLearningAssets(options = {}) {
|
|
408
|
+
const limit = normalizeLearningAssetLimit(options.limit);
|
|
409
|
+
if (!this.opts.senderId()?.trim())
|
|
410
|
+
return { assets: [], limit, reason: 'sender_id_required' };
|
|
411
|
+
try {
|
|
412
|
+
const body = await this.http.call('GET', '/a2a/learning-assets', undefined, learningAssetListQuery(options, limit));
|
|
413
|
+
const payload = asRecord(body['payload']) ?? body;
|
|
414
|
+
return {
|
|
415
|
+
assets: learningAssetsFromPayload(payload),
|
|
416
|
+
limit: numberField(payload, 'limit') ?? limit,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
catch (e) {
|
|
420
|
+
return { assets: [], limit, reason: failureReason(e) };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
async recordLearningAssetUsage(report) {
|
|
424
|
+
if (!this.opts.senderId()?.trim())
|
|
425
|
+
return { recorded: false, reason: 'sender_id_required', results: [] };
|
|
426
|
+
const sourceEventId = trimStringField(report.sourceEventId, 160);
|
|
427
|
+
if (!sourceEventId)
|
|
428
|
+
return { recorded: false, reason: 'source_event_id_required', results: [] };
|
|
429
|
+
const assetIds = normalizeLearningAssetIds(report.usedAssetIds && report.usedAssetIds.length > 0 ? report.usedAssetIds : [report.assetId]);
|
|
430
|
+
if (assetIds.length === 0)
|
|
431
|
+
return { recorded: false, reason: 'asset_id_required', results: [] };
|
|
432
|
+
const outcome = normalizeLearningAssetOutcome(report.outcome);
|
|
433
|
+
if (!outcome)
|
|
434
|
+
return { recorded: false, reason: 'invalid_learning_asset_outcome', results: [] };
|
|
435
|
+
const score = optionalLearningAssetScore(report.score);
|
|
436
|
+
if ('reason' in score)
|
|
437
|
+
return { recorded: false, reason: score.reason, results: [] };
|
|
438
|
+
const reason = trimStringField(report.reason, 2_000);
|
|
439
|
+
try {
|
|
440
|
+
const body = await this.http.call('POST', '/a2a/learning-assets/usage', {
|
|
441
|
+
...(assetIds.length === 1 ? { asset_id: assetIds[0] } : { used_asset_ids: assetIds }),
|
|
442
|
+
outcome,
|
|
443
|
+
source_event_id: sourceEventId,
|
|
444
|
+
...(score.value !== undefined ? { score: score.value } : {}),
|
|
445
|
+
...(reason ? { reason } : {}),
|
|
446
|
+
});
|
|
447
|
+
return learningAssetUsageReceiptFromBody(body);
|
|
448
|
+
}
|
|
449
|
+
catch (e) {
|
|
450
|
+
return { recorded: false, reason: failureReason(e), results: [] };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
405
453
|
/**
|
|
406
454
|
* Pre-publish dry-run (POST /a2a/validate). The hub runs the same hub-side quality +
|
|
407
455
|
* content-safety gate as publish but stores nothing and charges no credits. This adapter is
|
|
@@ -457,14 +505,14 @@ export class PublicHubCapability {
|
|
|
457
505
|
...(request.pricePerExecution !== undefined ? { price_per_execution: request.pricePerExecution } : {}),
|
|
458
506
|
...(request.currency ? { currency: request.currency } : {}),
|
|
459
507
|
...(request.maxConcurrent !== undefined ? { max_concurrent: request.maxConcurrent } : {}),
|
|
460
|
-
});
|
|
508
|
+
}, undefined, request.idempotencyKey ? { 'idempotency-key': request.idempotencyKey } : undefined);
|
|
461
509
|
return recipeReceiptFromBody(body);
|
|
462
510
|
}
|
|
463
|
-
async publishRecipe(recipeId) {
|
|
511
|
+
async publishRecipe(recipeId, options) {
|
|
464
512
|
if (isHubDryRunEnabled())
|
|
465
513
|
return dryRunRecipeReceipt('publish_recipe', recipeId);
|
|
466
514
|
const sender = this.opts.senderId();
|
|
467
|
-
const body = await this.http.call('POST', `/a2a/recipe/${encodeURIComponent(recipeId)}/publish`, { ...(sender ? { node_id: sender } : {}) });
|
|
515
|
+
const body = await this.http.call('POST', `/a2a/recipe/${encodeURIComponent(recipeId)}/publish`, { ...(sender ? { node_id: sender } : {}) }, undefined, options?.idempotencyKey ? { 'idempotency-key': options.idempotencyKey } : undefined);
|
|
468
516
|
return recipeReceiptFromBody(body);
|
|
469
517
|
}
|
|
470
518
|
async getRecipe(recipeId) {
|
|
@@ -765,6 +813,105 @@ function accountAssetsFromPayload(payload) {
|
|
|
765
813
|
}
|
|
766
814
|
return [];
|
|
767
815
|
}
|
|
816
|
+
function learningAssetsFromPayload(payload) {
|
|
817
|
+
const candidates = [
|
|
818
|
+
payload['assets'],
|
|
819
|
+
payload['results'],
|
|
820
|
+
payload['items'],
|
|
821
|
+
];
|
|
822
|
+
for (const candidate of candidates) {
|
|
823
|
+
if (!Array.isArray(candidate))
|
|
824
|
+
continue;
|
|
825
|
+
return candidate.filter((asset) => isLearningAssetRecord(asset));
|
|
826
|
+
}
|
|
827
|
+
return [];
|
|
828
|
+
}
|
|
829
|
+
function isLearningAssetRecord(value) {
|
|
830
|
+
const record = asRecord(value);
|
|
831
|
+
return Boolean(record && typeof record['asset_id'] === 'string' && typeof record['type'] === 'string');
|
|
832
|
+
}
|
|
833
|
+
function normalizeLearningAssetLimit(value) {
|
|
834
|
+
if (!Number.isFinite(value))
|
|
835
|
+
return 20;
|
|
836
|
+
return Math.min(100, Math.max(1, Math.floor(value)));
|
|
837
|
+
}
|
|
838
|
+
function learningAssetListQuery(options, limit) {
|
|
839
|
+
const status = normalizeLearningAssetStatusParam(options.status);
|
|
840
|
+
const query = {
|
|
841
|
+
limit,
|
|
842
|
+
runtime: options.includeExpired === true ? undefined : 'true',
|
|
843
|
+
include_expired: options.includeExpired === true ? 'true' : undefined,
|
|
844
|
+
include_payload: options.includePayload === true ? 'true' : undefined,
|
|
845
|
+
...(options.type ? { type: options.type } : {}),
|
|
846
|
+
...(status ? { status } : options.includeExpired === true ? { status: 'active' } : {}),
|
|
847
|
+
};
|
|
848
|
+
const scope = compactLearningAssetParam(options.scope);
|
|
849
|
+
if (scope)
|
|
850
|
+
query['scope'] = scope;
|
|
851
|
+
return query;
|
|
852
|
+
}
|
|
853
|
+
function normalizeLearningAssetStatusParam(status) {
|
|
854
|
+
if (Array.isArray(status))
|
|
855
|
+
return compactLearningAssetParam(status);
|
|
856
|
+
return typeof status === 'string' && status.trim() ? status.trim() : undefined;
|
|
857
|
+
}
|
|
858
|
+
function compactLearningAssetParam(values) {
|
|
859
|
+
if (!values)
|
|
860
|
+
return undefined;
|
|
861
|
+
const out = [...new Set(values.map((value) => String(value).trim()).filter(Boolean))];
|
|
862
|
+
return out.length > 0 ? out.join(',') : undefined;
|
|
863
|
+
}
|
|
864
|
+
function trimStringField(value, maxLen) {
|
|
865
|
+
return typeof value === 'string' && value.trim() ? value.trim().slice(0, maxLen) : undefined;
|
|
866
|
+
}
|
|
867
|
+
function normalizeLearningAssetIds(values) {
|
|
868
|
+
const out = [];
|
|
869
|
+
const seen = new Set();
|
|
870
|
+
for (const value of values) {
|
|
871
|
+
if (typeof value !== 'string')
|
|
872
|
+
continue;
|
|
873
|
+
const trimmed = value.trim();
|
|
874
|
+
if (!trimmed || trimmed.length > LEARNING_ASSET_ID_MAX_LEN || seen.has(trimmed))
|
|
875
|
+
continue;
|
|
876
|
+
seen.add(trimmed);
|
|
877
|
+
out.push(trimmed);
|
|
878
|
+
if (out.length >= LEARNING_ASSET_IDS_MAX)
|
|
879
|
+
break;
|
|
880
|
+
}
|
|
881
|
+
return out;
|
|
882
|
+
}
|
|
883
|
+
function normalizeLearningAssetOutcome(value) {
|
|
884
|
+
if (value === 'success' || value === 'failed' || value === 'mismatched' || value === 'stale' || value === 'unsafe')
|
|
885
|
+
return value;
|
|
886
|
+
return undefined;
|
|
887
|
+
}
|
|
888
|
+
function optionalLearningAssetScore(value) {
|
|
889
|
+
if (value === undefined)
|
|
890
|
+
return { value: undefined };
|
|
891
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1)
|
|
892
|
+
return { reason: 'invalid_score' };
|
|
893
|
+
return { value };
|
|
894
|
+
}
|
|
895
|
+
function learningAssetUsageReceiptFromBody(body) {
|
|
896
|
+
const payload = asRecord(body['payload']) ?? body;
|
|
897
|
+
const rows = Array.isArray(payload['results'])
|
|
898
|
+
? payload['results'].filter((row) => Boolean(asRecord(row)))
|
|
899
|
+
: [];
|
|
900
|
+
const reason = stringField(payload, 'reason') ?? stringField(payload, 'error');
|
|
901
|
+
return {
|
|
902
|
+
recorded: rows.length > 0 && rows.some((row) => row.recorded === true),
|
|
903
|
+
...(reason ? { reason } : {}),
|
|
904
|
+
results: rows,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function failureReason(error) {
|
|
908
|
+
if (error instanceof HubClientError) {
|
|
909
|
+
const body = asRecord(error.body) ?? {};
|
|
910
|
+
const payload = asRecord(body['payload']) ?? body;
|
|
911
|
+
return stringField(payload, 'reason') ?? stringField(payload, 'error') ?? `hub ${error.status}`;
|
|
912
|
+
}
|
|
913
|
+
return error instanceof Error ? error.message : String(error);
|
|
914
|
+
}
|
|
768
915
|
function assetMatchesId(asset, assetId) {
|
|
769
916
|
return Boolean(asset && (asset.asset_id === assetId || stringField(asset, 'id') === assetId));
|
|
770
917
|
}
|
package/dist/hubFetch.d.ts
CHANGED
|
@@ -69,7 +69,7 @@ export interface HubFetchDeps {
|
|
|
69
69
|
export declare class HubFetch {
|
|
70
70
|
private readonly deps;
|
|
71
71
|
constructor(deps: HubFetchDeps);
|
|
72
|
-
call<T>(method: string, path: string, bodyObj?: Record<string, unknown>, query?: Record<string, string | number | undefined
|
|
72
|
+
call<T>(method: string, path: string, bodyObj?: Record<string, unknown>, query?: Record<string, string | number | undefined>, requestHeaders?: Readonly<Record<string, string>>): Promise<T>;
|
|
73
73
|
}
|
|
74
74
|
export declare function hubResponseContentType(res: Pick<HubFetchResponse, 'headers'> | undefined): string;
|
|
75
75
|
export declare function isHubApiResponse(res: Pick<HubFetchResponse, 'headers'> | undefined): boolean;
|
package/dist/hubFetch.js
CHANGED
|
@@ -40,6 +40,24 @@ export class HubUnreachableError extends Error {
|
|
|
40
40
|
return this.details.retryAfterMs ?? HUB_UNREACHABLE_BACKOFF_BASE_MS;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
const PROTECTED_REQUEST_HEADERS = new Set([
|
|
44
|
+
'authorization',
|
|
45
|
+
'content-type',
|
|
46
|
+
'x-evomap-node-secret-version',
|
|
47
|
+
'x-evomap-signature',
|
|
48
|
+
'x-node-secret',
|
|
49
|
+
]);
|
|
50
|
+
function mergeRequestHeaders(requestHeaders, signedHeaders) {
|
|
51
|
+
const signedNames = new Set(Object.keys(signedHeaders ?? {}).map((name) => name.toLowerCase()));
|
|
52
|
+
const headers = {};
|
|
53
|
+
for (const [name, value] of Object.entries(requestHeaders ?? {})) {
|
|
54
|
+
const normalized = name.toLowerCase();
|
|
55
|
+
if (!PROTECTED_REQUEST_HEADERS.has(normalized) && !signedNames.has(normalized))
|
|
56
|
+
headers[normalized] = value;
|
|
57
|
+
}
|
|
58
|
+
headers['content-type'] = 'application/json';
|
|
59
|
+
return { ...headers, ...signedHeaders };
|
|
60
|
+
}
|
|
43
61
|
/**
|
|
44
62
|
* 公版 hub HTTP 客户端(M6-6). 每请求经 AuthProvider 取凭证: POST 通常注入 body; GET 与 strict hello envelope
|
|
45
63
|
* 走 **Authorization: Bearer <node_secret>** 头(hub 只从 header/body 读 node_secret, 绝不从 query — #8);
|
|
@@ -52,7 +70,7 @@ export class HubFetch {
|
|
|
52
70
|
constructor(deps) {
|
|
53
71
|
this.deps = deps;
|
|
54
72
|
}
|
|
55
|
-
async call(method, path, bodyObj, query) {
|
|
73
|
+
async call(method, path, bodyObj, query, requestHeaders) {
|
|
56
74
|
const draft = bodyObj !== undefined ? JSON.stringify(bodyObj) : '';
|
|
57
75
|
const signed = await this.deps.auth.authenticate({ method, path, ...(draft ? { body: draft } : {}) });
|
|
58
76
|
const sender = this.deps.senderId();
|
|
@@ -60,7 +78,7 @@ export class HubFetch {
|
|
|
60
78
|
let url = `${this.deps.baseUrl}${path}`;
|
|
61
79
|
assertHubUrlSecure(url); // request-level scheme guard (defense in depth): even a misconfigured injected fetchFn cannot egress in plaintext
|
|
62
80
|
let body;
|
|
63
|
-
const headers =
|
|
81
|
+
const headers = mergeRequestHeaders(requestHeaders, signed.headers);
|
|
64
82
|
if (method === 'GET') {
|
|
65
83
|
const qs = new URLSearchParams();
|
|
66
84
|
if (sender)
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export * from './antiAbuseTelemetry.js';
|
|
|
14
14
|
export * from './offlinePermit.js';
|
|
15
15
|
export * from './hubReuse.js';
|
|
16
16
|
export * from './hubUrl.js';
|
|
17
|
+
export * from './learningPacketSink.js';
|
|
17
18
|
export * from './atp.js';
|
|
18
19
|
export * from './pricing/modelPrices.js';
|
|
19
20
|
export * from './connect.js';
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,7 @@ export * from './antiAbuseTelemetry.js';
|
|
|
14
14
|
export * from './offlinePermit.js';
|
|
15
15
|
export * from './hubReuse.js';
|
|
16
16
|
export * from './hubUrl.js';
|
|
17
|
+
export * from './learningPacketSink.js';
|
|
17
18
|
export * from './atp.js';
|
|
18
19
|
export * from './pricing/modelPrices.js';
|
|
19
20
|
export * from './connect.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { hub, trace } from '@evomap/evolver-core';
|
|
2
|
+
import { type FetchLike } from './hubFetch.js';
|
|
3
|
+
/** Hub traceEvents array cap (createLearningPacketSchema). Extra events are dropped, noted in metadata. */
|
|
4
|
+
export declare const HUB_TRACE_EVENTS_MAX = 100;
|
|
5
|
+
export interface HubLearningPacketSinkOptions {
|
|
6
|
+
baseUrl: string;
|
|
7
|
+
auth: hub.AuthProvider;
|
|
8
|
+
fetchFn: FetchLike;
|
|
9
|
+
/** Optional node identity recorded on the packet (hub nodeId column). */
|
|
10
|
+
nodeId?: () => string | undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Deterministic content hash over the draft body (hub contentHash column, dedup aid). */
|
|
13
|
+
export declare function learningPacketContentHash(draft: trace.LearningPacketDraft): string;
|
|
14
|
+
/** Map a core draft to the hub createLearningPacket body. Exported for tests and for the private adapter to reuse. */
|
|
15
|
+
export declare function learningPacketWireBody(draft: trace.LearningPacketDraft, nodeId?: string): Record<string, unknown>;
|
|
16
|
+
/**
|
|
17
|
+
* LearningPacketSink implementation against the public hub Learning Ops ingest API. Best-effort by
|
|
18
|
+
* contract: every failure returns { accepted: false, reason } (never throws) — the runtime treats packet
|
|
19
|
+
* delivery as observability, so a hub outage must never affect a task verdict. A 409 duplicate_source is
|
|
20
|
+
* reported as accepted (the packet is already there; the idempotency key did its job).
|
|
21
|
+
*/
|
|
22
|
+
export declare class HubLearningPacketSink implements trace.LearningPacketSink {
|
|
23
|
+
private readonly opts;
|
|
24
|
+
constructor(opts: HubLearningPacketSinkOptions);
|
|
25
|
+
submit(draft: trace.LearningPacketDraft): Promise<trace.LearningPacketSubmitResult>;
|
|
26
|
+
}
|
|
27
|
+
/** Fan-out: always deliver to `primary` (local file record), then best-effort to `secondary` (hub upload).
|
|
28
|
+
* The composite result reflects the PRIMARY sink — the local record is the durability guarantee. */
|
|
29
|
+
export declare class TeeLearningPacketSink implements trace.LearningPacketSink {
|
|
30
|
+
private readonly primary;
|
|
31
|
+
private readonly secondary;
|
|
32
|
+
constructor(primary: trace.LearningPacketSink, secondary: trace.LearningPacketSink);
|
|
33
|
+
submit(draft: trace.LearningPacketDraft): Promise<trace.LearningPacketSubmitResult>;
|
|
34
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Learning Ops packet upload (slice 3) — the adapter half of core's LearningPacketSink port.
|
|
2
|
+
// Maps a local LearningPacketDraft (learning_packet.v0, built by evolver-core/trace) onto the hub's
|
|
3
|
+
// `POST /api/learning-packets` ingest contract (strict zod schema, requireAuth Bearer token).
|
|
4
|
+
//
|
|
5
|
+
// Deliberately NOT built on HubFetch: that helper injects sender_id/credential fields into every POST body
|
|
6
|
+
// (the /a2a envelope convention), which the strict learning-packets schema rejects. This sink authenticates
|
|
7
|
+
// via the injected AuthProvider (Authorization header only) and sends exactly the schema's fields.
|
|
8
|
+
import { createHash } from 'node:crypto';
|
|
9
|
+
import { assertHubUrlSecure, isHubUnreachableError } from './hubFetch.js';
|
|
10
|
+
/** Hub traceEvents array cap (createLearningPacketSchema). Extra events are dropped, noted in metadata. */
|
|
11
|
+
export const HUB_TRACE_EVENTS_MAX = 100;
|
|
12
|
+
/** Hub failureCategory is a closed enum; runtime failureKind is looser. Only the sure mapping is direct. */
|
|
13
|
+
function failureCategoryFor(failureKind) {
|
|
14
|
+
if (failureKind === null)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (failureKind === 'permission_denied')
|
|
17
|
+
return 'permission_error';
|
|
18
|
+
if (failureKind === 'timeout' || failureKind === 'non_zero_exit' || failureKind === 'invalid_output')
|
|
19
|
+
return 'tool_error';
|
|
20
|
+
return 'other';
|
|
21
|
+
}
|
|
22
|
+
function outcomeStatusFor(status) {
|
|
23
|
+
if (status === 'success')
|
|
24
|
+
return 'succeeded';
|
|
25
|
+
if (status === 'failed')
|
|
26
|
+
return 'failed';
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
/** Deterministic content hash over the draft body (hub contentHash column, dedup aid). */
|
|
30
|
+
export function learningPacketContentHash(draft) {
|
|
31
|
+
return `sha256:${createHash('sha256').update(JSON.stringify(draft)).digest('hex')}`;
|
|
32
|
+
}
|
|
33
|
+
/** Map a core draft to the hub createLearningPacket body. Exported for tests and for the private adapter to reuse. */
|
|
34
|
+
export function learningPacketWireBody(draft, nodeId) {
|
|
35
|
+
const truncated = draft.trajectory.length > HUB_TRACE_EVENTS_MAX;
|
|
36
|
+
const events = draft.trajectory.slice(0, HUB_TRACE_EVENTS_MAX);
|
|
37
|
+
const outcomeStatus = outcomeStatusFor(draft.evaluation.outcomeStatus);
|
|
38
|
+
const failureCategory = failureCategoryFor(draft.evaluation.failureCategory);
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: draft.schemaVersion,
|
|
41
|
+
status: 'draft',
|
|
42
|
+
sourceRepo: draft.source.repo,
|
|
43
|
+
sourceRun: draft.source.run,
|
|
44
|
+
sourceType: draft.source.type,
|
|
45
|
+
sourceId: draft.source.id,
|
|
46
|
+
// One packet per run: the run id IS the idempotency key, so a retried submit dedups hub-side (409).
|
|
47
|
+
idempotencyKey: `${draft.source.repo}:${draft.source.run}`,
|
|
48
|
+
contentHash: learningPacketContentHash(draft),
|
|
49
|
+
...(nodeId ? { nodeId } : {}),
|
|
50
|
+
...(outcomeStatus ? { outcomeStatus } : {}),
|
|
51
|
+
...(failureCategory ? { failureCategory } : {}),
|
|
52
|
+
...(draft.task.summary !== null ? { summary: draft.task.summary } : {}),
|
|
53
|
+
payload: {
|
|
54
|
+
task: draft.task,
|
|
55
|
+
context: draft.context,
|
|
56
|
+
artifacts: draft.artifacts,
|
|
57
|
+
evaluation: draft.evaluation,
|
|
58
|
+
governance: draft.governance,
|
|
59
|
+
},
|
|
60
|
+
metadata: {
|
|
61
|
+
...(truncated ? { traceEventsTruncated: true, traceEventsTotal: draft.trajectory.length } : {}),
|
|
62
|
+
...(draft.evaluation.failureCategory !== null ? { runtimeFailureKind: draft.evaluation.failureCategory } : {}),
|
|
63
|
+
},
|
|
64
|
+
redactionStatus: draft.governance.redactionStatus,
|
|
65
|
+
consentStatus: draft.governance.consentStatus,
|
|
66
|
+
retentionPolicy: draft.governance.retentionPolicy,
|
|
67
|
+
traceEvents: events.map((e) => ({
|
|
68
|
+
eventId: e.eventId,
|
|
69
|
+
schemaVersion: e.schemaVersion,
|
|
70
|
+
eventType: e.eventType,
|
|
71
|
+
occurredAt: e.occurredAt,
|
|
72
|
+
traceId: e.traceId,
|
|
73
|
+
...(e.sessionId !== undefined ? { sessionId: e.sessionId } : {}),
|
|
74
|
+
...(e.taskId !== undefined ? { taskId: e.taskId } : {}),
|
|
75
|
+
sequence: e.sequence,
|
|
76
|
+
payload: e.payload,
|
|
77
|
+
metadata: e.metadata,
|
|
78
|
+
})),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* LearningPacketSink implementation against the public hub Learning Ops ingest API. Best-effort by
|
|
83
|
+
* contract: every failure returns { accepted: false, reason } (never throws) — the runtime treats packet
|
|
84
|
+
* delivery as observability, so a hub outage must never affect a task verdict. A 409 duplicate_source is
|
|
85
|
+
* reported as accepted (the packet is already there; the idempotency key did its job).
|
|
86
|
+
*/
|
|
87
|
+
export class HubLearningPacketSink {
|
|
88
|
+
opts;
|
|
89
|
+
constructor(opts) {
|
|
90
|
+
this.opts = opts;
|
|
91
|
+
}
|
|
92
|
+
async submit(draft) {
|
|
93
|
+
try {
|
|
94
|
+
const url = `${this.opts.baseUrl}/api/learning-packets`;
|
|
95
|
+
assertHubUrlSecure(url);
|
|
96
|
+
const signed = await this.opts.auth.authenticate({ method: 'POST', path: '/api/learning-packets' });
|
|
97
|
+
// The strict learning-packets schema rejects extra body fields, so a legacy body credential
|
|
98
|
+
// (bodyFields.node_secret) is promoted to Authorization: Bearer — the header requireAuth reads.
|
|
99
|
+
const headers = { 'content-type': 'application/json', ...(signed.headers ?? {}) };
|
|
100
|
+
const bodySecret = signed.bodyFields?.['node_secret'];
|
|
101
|
+
if (headers['authorization'] === undefined && bodySecret !== undefined)
|
|
102
|
+
headers['authorization'] = `Bearer ${String(bodySecret)}`;
|
|
103
|
+
const res = await this.opts.fetchFn(url, {
|
|
104
|
+
method: 'POST',
|
|
105
|
+
headers,
|
|
106
|
+
body: JSON.stringify(learningPacketWireBody(draft, this.opts.nodeId?.())),
|
|
107
|
+
});
|
|
108
|
+
if (res.status === 201) {
|
|
109
|
+
const body = await res.json().catch(() => null);
|
|
110
|
+
const packet = body && typeof body === 'object' ? body.packet : undefined;
|
|
111
|
+
return { accepted: true, ...(typeof packet?.id === 'string' ? { reason: packet.id } : {}) };
|
|
112
|
+
}
|
|
113
|
+
if (res.status === 409)
|
|
114
|
+
return { accepted: true, reason: 'duplicate_source' };
|
|
115
|
+
const text = await res.text().catch(() => '');
|
|
116
|
+
return { accepted: false, reason: `hub ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}` };
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
if (isHubUnreachableError(e))
|
|
120
|
+
return { accepted: false, reason: 'hub_unreachable' };
|
|
121
|
+
return { accepted: false, reason: e instanceof Error ? e.message : String(e) };
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Fan-out: always deliver to `primary` (local file record), then best-effort to `secondary` (hub upload).
|
|
126
|
+
* The composite result reflects the PRIMARY sink — the local record is the durability guarantee. */
|
|
127
|
+
export class TeeLearningPacketSink {
|
|
128
|
+
primary;
|
|
129
|
+
secondary;
|
|
130
|
+
constructor(primary, secondary) {
|
|
131
|
+
this.primary = primary;
|
|
132
|
+
this.secondary = secondary;
|
|
133
|
+
}
|
|
134
|
+
async submit(draft) {
|
|
135
|
+
const primary = await this.primary.submit(draft);
|
|
136
|
+
try {
|
|
137
|
+
await this.secondary.submit(draft);
|
|
138
|
+
}
|
|
139
|
+
catch { /* secondary is best-effort by contract; sinks should not throw, but never let one break the record */ }
|
|
140
|
+
return primary;
|
|
141
|
+
}
|
|
142
|
+
}
|
package/dist/wireMap.js
CHANGED
|
@@ -62,11 +62,17 @@ export function atpRetryClass(status) {
|
|
|
62
62
|
export function publishRespToReceipt(status, body) {
|
|
63
63
|
const payload = body['payload'] ?? body;
|
|
64
64
|
const assetIds = payload['asset_ids'];
|
|
65
|
-
const
|
|
65
|
+
const targetAssetId = payload['target_asset_id']
|
|
66
|
+
?? body['target_asset_id'];
|
|
67
|
+
const assetId = (status === 409 ? targetAssetId : undefined)
|
|
68
|
+
?? payload['asset_id']
|
|
69
|
+
?? body['asset_id']
|
|
70
|
+
?? assetIds?.[0]
|
|
71
|
+
?? targetAssetId;
|
|
66
72
|
const bundleId = payload['bundle_id'];
|
|
67
73
|
if (status >= 200 && status < 300) {
|
|
68
74
|
const decision = String(payload['decision'] ?? payload['status'] ?? 'accepted');
|
|
69
|
-
const accepted = decision === 'accepted' || decision === 'approved' || decision === 'ok';
|
|
75
|
+
const accepted = decision === 'accept' || decision === 'accepted' || decision === 'approved' || decision === 'ok';
|
|
70
76
|
return {
|
|
71
77
|
receiptId: String(payload['receipt_id'] ?? bundleId ?? payload['id'] ?? assetId ?? 'unknown'),
|
|
72
78
|
status: accepted ? 'accepted' : (decision === 'quarantine' ? 'quarantine' : 'rejected'),
|
|
@@ -85,6 +91,7 @@ export function publishRespToReceipt(status, body) {
|
|
|
85
91
|
status: 'rejected',
|
|
86
92
|
reason: String(payload['reason'] ?? reasonByStatus[status] ?? `hub ${status}`),
|
|
87
93
|
...(assetId ? { assetId } : {}),
|
|
94
|
+
...(assetIds ? { assetIds } : {}),
|
|
88
95
|
terminal: true,
|
|
89
96
|
};
|
|
90
97
|
if (status === 402) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-adapter-public",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "公版 hub 适配器 (积分/治理)",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
16
|
"@evomap/atp-sdk": "^0.1.0",
|
|
17
|
-
"@evomap/evolver-core": "2.0.0-beta.
|
|
17
|
+
"@evomap/evolver-core": "2.0.0-beta.16",
|
|
18
18
|
"undici": "^6.27.0"
|
|
19
19
|
},
|
|
20
20
|
"optionalDependencies": {
|