@evomap/evolver-adapter-public 2.0.0-beta.9 → 2.0.0
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/antiAbuseTelemetry.js +2 -1
- package/dist/auth/credentialStore.d.ts +87 -3
- package/dist/auth/credentialStore.js +1065 -10
- package/dist/auth/oauthDeviceToken.d.ts +9 -6
- package/dist/auth/oauthDeviceToken.js +71 -18
- package/dist/auth/oauthHttpTransport.d.ts +4 -0
- package/dist/auth/oauthHttpTransport.js +66 -15
- package/dist/auth/windowsPowerShell.d.ts +3 -0
- package/dist/auth/windowsPowerShell.js +91 -0
- package/dist/hubCapability.d.ts +16 -4
- package/dist/hubCapability.js +316 -39
- package/dist/hubFetch.d.ts +44 -11
- package/dist/hubFetch.js +329 -76
- package/dist/hubReuse.d.ts +40 -0
- package/dist/hubReuse.js +303 -32
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/learningPacketFeedback.d.ts +68 -0
- package/dist/learningPacketFeedback.js +104 -0
- package/dist/learningPacketSink.d.ts +40 -0
- package/dist/learningPacketSink.js +153 -0
- package/dist/wireMap.d.ts +3 -1
- package/dist/wireMap.js +29 -3
- package/package.json +6 -3
package/dist/hubCapability.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { bootstrap, hub as hubNs, signals } from '@evomap/evolver-core';
|
|
2
3
|
import { AuthError, HubFetch, HubClientError, isHubUnreachableError } from './hubFetch.js';
|
|
3
4
|
import { isNodeSecret, parseNodeSecretVersion } from './auth/legacyShim.js';
|
|
4
|
-
import { inboundToAgentEvent, agentEventToOutbound, publishRespToReceipt, searchQueryToFetchWire } from './wireMap.js';
|
|
5
|
+
import { inboundToAgentEvent, agentEventToOutbound, publishRespToReceipt, searchQueryToFetchWire, searchQueryToSearchOnlyWire, } from './wireMap.js';
|
|
5
6
|
import { antiAbuseTelemetryMode, buildHeartbeatAntiAbuseTelemetry, } from './antiAbuseTelemetry.js';
|
|
6
7
|
import { getWorkspaceKeychainMode } from './auth/workspaceKeychain.js';
|
|
7
8
|
import { agentDirectoryFailure, parsePublicAgentPage, parsePublicAgentProfile, paginatePublicAgentPage, mergePublicAgentPages, publicAgentSearchQuery, publicTaskDiscoveryQuery, PUBLIC_TASK_DISCOVERY_MAX_CANDIDATES, unsupportedPublicAvailability, unsupportedPublicSort, withDirectoryTimeout, } from './agentDirectory.js';
|
|
9
|
+
import { assetMatchesId } from './hubReuse.js';
|
|
8
10
|
export const INBOUND_LIMIT = 100;
|
|
9
11
|
export const OUTBOUND_MAX_BATCH = 50;
|
|
10
12
|
export const OUTBOUND_MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
11
13
|
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'];
|
|
14
|
+
export const PUBLIC_HUB_CAPABILITIES = ['publish', 'fetch', 'search', 'task', 'mailbox', 'auth', 'marketplace', 'economy', 'questions', 'recipes', 'agent_directory', 'learning_assets'];
|
|
13
15
|
const QUESTION_SUBMIT_FAST_PATH_BYPASS_CONTENT_HASH = 'sha256:0000000000000000000000000000000000000000000000000000000000000000';
|
|
14
16
|
const DRY_RUN_RECIPE_ID = 'dry-run-recipe';
|
|
15
17
|
const HUB_DRY_RUN_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
@@ -18,14 +20,23 @@ const HUB_DRY_RUN_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
|
18
20
|
// equals what the hub will KEEP.
|
|
19
21
|
export const USED_ASSET_IDS_MAX = 50;
|
|
20
22
|
export const USED_ASSET_ID_MAX_LEN = 200;
|
|
23
|
+
export const LEARNING_ASSET_IDS_MAX = 50;
|
|
24
|
+
export const LEARNING_ASSET_ID_MAX_LEN = 128;
|
|
21
25
|
/** 完整 GEP-A2A 信封(实测 dev: publish/fetch/validate 等协议消息端点必须全信封, 非仅 protocol+message_type). */
|
|
22
|
-
export function gepEnvelope(messageType, payload) {
|
|
26
|
+
export function gepEnvelope(messageType, payload, options = {}) {
|
|
23
27
|
return {
|
|
24
28
|
protocol: 'gep-a2a', protocol_version: '1.0.0', message_type: messageType,
|
|
25
|
-
message_id: `msg_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`,
|
|
29
|
+
message_id: options.messageId ?? `msg_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`,
|
|
26
30
|
timestamp: new Date().toISOString(), payload,
|
|
27
31
|
};
|
|
28
32
|
}
|
|
33
|
+
function stablePublishMessageId(idempotencyKey) {
|
|
34
|
+
const digest = createHash('sha256')
|
|
35
|
+
.update(idempotencyKey.trim())
|
|
36
|
+
.digest('hex')
|
|
37
|
+
.slice(0, 40);
|
|
38
|
+
return `msg_idem_${digest}`;
|
|
39
|
+
}
|
|
29
40
|
// v1 a2aProtocol.js L1999-2003: the three app-level rejection reasons that mean
|
|
30
41
|
// our cached node_secret has DIVERGED from the hub's record (hub-side reset,
|
|
31
42
|
// restored-from-backup machine, manual unlink) — not a transport/generic failure.
|
|
@@ -83,7 +94,7 @@ export class PublicHubCapability {
|
|
|
83
94
|
auth;
|
|
84
95
|
recipes = {
|
|
85
96
|
create: async (request) => this.createRecipe(request),
|
|
86
|
-
publish: async (recipeId) => this.publishRecipe(recipeId),
|
|
97
|
+
publish: async (recipeId, options) => this.publishRecipe(recipeId, options),
|
|
87
98
|
get: async (recipeId) => this.getRecipe(recipeId),
|
|
88
99
|
express: async (recipeId, request = {}) => this.expressRecipe(recipeId, request),
|
|
89
100
|
};
|
|
@@ -92,9 +103,27 @@ export class PublicHubCapability {
|
|
|
92
103
|
this.auth = opts.auth;
|
|
93
104
|
this.http = new HubFetch({ baseUrl: opts.baseUrl, auth: opts.auth, fetchFn: opts.fetchFn, senderId: opts.senderId });
|
|
94
105
|
}
|
|
106
|
+
evolverVersionForWire(explicitVersion) {
|
|
107
|
+
const antiAbuse = this.opts.antiAbuse;
|
|
108
|
+
return bootstrap.normalizeEvolverVersion(explicitVersion !== undefined
|
|
109
|
+
? explicitVersion
|
|
110
|
+
: antiAbuse?.evolverVersion ?? antiAbuse?.envFingerprint?.evolver_version);
|
|
111
|
+
}
|
|
112
|
+
envFingerprintForWire(evolverVersion) {
|
|
113
|
+
const fingerprint = {
|
|
114
|
+
...(this.opts.antiAbuse?.envFingerprint
|
|
115
|
+
?? bootstrap.captureEnvFingerprint({ env: this.opts.antiAbuse?.env ?? process.env })),
|
|
116
|
+
};
|
|
117
|
+
if (evolverVersion)
|
|
118
|
+
fingerprint.evolver_version = evolverVersion;
|
|
119
|
+
else
|
|
120
|
+
delete fingerprint.evolver_version;
|
|
121
|
+
return fingerprint;
|
|
122
|
+
}
|
|
95
123
|
async hello(opts) {
|
|
96
124
|
try {
|
|
97
125
|
const sender = this.opts.senderId();
|
|
126
|
+
const evolverVersion = this.evolverVersionForWire(opts.evolverVersion);
|
|
98
127
|
const body = await this.http.call('POST', '/a2a/hello', gepEnvelope('hello', {
|
|
99
128
|
rotate_secret: opts.rotate,
|
|
100
129
|
capabilities: { supported_types: ['publish', 'fetch', 'mailbox', 'questions'] },
|
|
@@ -102,12 +131,12 @@ export class PublicHubCapability {
|
|
|
102
131
|
status: 'active',
|
|
103
132
|
timestamp: new Date().toISOString(),
|
|
104
133
|
...(sender ? { node_id: sender } : {}),
|
|
105
|
-
...(
|
|
134
|
+
...(evolverVersion ? { evolver_version: evolverVersion } : {}),
|
|
106
135
|
// v1 parity (a2aProtocol.js buildHello): every hello carries the env fingerprint — it is how the
|
|
107
136
|
// hub builds node/IP trust for its anti-abuse layer. v2 had moved it to heartbeat-only meta, which
|
|
108
137
|
// one-shot CLI paths never send; the hub then answers heartbeats with resend_hello
|
|
109
138
|
// `missing_env_fingerprint` and 403-antibodies /a2a/fetch (#555).
|
|
110
|
-
env_fingerprint:
|
|
139
|
+
env_fingerprint: this.envFingerprintForWire(evolverVersion),
|
|
111
140
|
}));
|
|
112
141
|
const payload = asRecord(body['payload']) ?? body;
|
|
113
142
|
const retryAfterMs = numberField(payload, 'retry_after_ms') ?? numberField(payload, 'retryAfterMs');
|
|
@@ -141,6 +170,8 @@ export class PublicHubCapability {
|
|
|
141
170
|
?? this.opts.senderId();
|
|
142
171
|
const nodeSecret = stringField(payload, 'node_secret') ?? stringField(payload, 'nodeSecret');
|
|
143
172
|
const nodeSecretVersion = parseNodeSecretVersion(payload['node_secret_version'] ?? payload['nodeSecretVersion']);
|
|
173
|
+
const claimCode = stringField(payload, 'claim_code') ?? stringField(payload, 'claimCode');
|
|
174
|
+
const claimUrl = stringField(payload, 'claim_url') ?? stringField(payload, 'claimUrl');
|
|
144
175
|
if (!opts.preserveCredentials) {
|
|
145
176
|
if (nodeSecret && isNodeSecret(nodeSecret)) {
|
|
146
177
|
this.auth.adoptNodeSecret?.(nodeSecret, nodeSecretVersion);
|
|
@@ -152,6 +183,8 @@ export class PublicHubCapability {
|
|
|
152
183
|
return {
|
|
153
184
|
ok: payload['ok'] !== false && Boolean(nodeId),
|
|
154
185
|
...(nodeId ? { nodeId } : {}),
|
|
186
|
+
...(claimCode ? { claimCode } : {}),
|
|
187
|
+
...(claimUrl ? { claimUrl } : {}),
|
|
155
188
|
...(nodeSecretVersion !== undefined ? { nodeSecretVersion } : {}),
|
|
156
189
|
...(rateLimitUntilMs !== undefined ? { rateLimitUntilMs } : {}),
|
|
157
190
|
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
@@ -170,12 +203,13 @@ export class PublicHubCapability {
|
|
|
170
203
|
async heartbeat(opts = {}) {
|
|
171
204
|
try {
|
|
172
205
|
const nodeSecretVersion = this.auth.getNodeSecretVersion?.();
|
|
173
|
-
const
|
|
206
|
+
const evolverVersion = this.evolverVersionForWire(opts.evolverVersion);
|
|
207
|
+
const meta = this.heartbeatMeta(evolverVersion, nodeSecretVersion);
|
|
174
208
|
const body = await this.http.call('POST', '/a2a/heartbeat', {
|
|
175
209
|
...(this.opts.senderId() ? { node_id: this.opts.senderId() } : {}),
|
|
176
210
|
timestamp: new Date().toISOString(),
|
|
177
211
|
status: 'active',
|
|
178
|
-
...(
|
|
212
|
+
...(evolverVersion ? { evolver_version: evolverVersion } : {}),
|
|
179
213
|
...(opts.lastUpdate ? { last_update: opts.lastUpdate } : {}),
|
|
180
214
|
...(nodeSecretVersion !== undefined ? { node_secret_version: nodeSecretVersion } : {}),
|
|
181
215
|
...(meta ? { meta } : {}),
|
|
@@ -192,7 +226,7 @@ export class PublicHubCapability {
|
|
|
192
226
|
throw err;
|
|
193
227
|
}
|
|
194
228
|
}
|
|
195
|
-
heartbeatMeta(
|
|
229
|
+
heartbeatMeta(evolverVersion, nodeSecretVersion) {
|
|
196
230
|
const meta = {};
|
|
197
231
|
if (nodeSecretVersion !== undefined)
|
|
198
232
|
meta['node_secret_version'] = nodeSecretVersion;
|
|
@@ -202,7 +236,7 @@ export class PublicHubCapability {
|
|
|
202
236
|
meta['anti_abuse'] = buildHeartbeatAntiAbuseTelemetry({
|
|
203
237
|
...antiAbuse,
|
|
204
238
|
nodeId: this.opts.senderId(),
|
|
205
|
-
evolverVersion
|
|
239
|
+
evolverVersion,
|
|
206
240
|
});
|
|
207
241
|
}
|
|
208
242
|
catch (err) {
|
|
@@ -213,15 +247,29 @@ export class PublicHubCapability {
|
|
|
213
247
|
}
|
|
214
248
|
return Object.keys(meta).length > 0 ? meta : undefined;
|
|
215
249
|
}
|
|
216
|
-
async publish(bundle) {
|
|
250
|
+
async publish(bundle, options = {}) {
|
|
251
|
+
const normalizedIdempotencyKey = options.idempotencyKey?.trim();
|
|
252
|
+
if (options.idempotencyKey !== undefined && !normalizedIdempotencyKey) {
|
|
253
|
+
return {
|
|
254
|
+
receiptId: 'local_invalid_idempotency_key',
|
|
255
|
+
status: 'rejected',
|
|
256
|
+
terminal: true,
|
|
257
|
+
reason: 'publish idempotency key must not be blank',
|
|
258
|
+
};
|
|
259
|
+
}
|
|
217
260
|
try {
|
|
218
261
|
// 公版 /a2a/publish 收 payload.assets=[Gene,Capsule,(Event)] 捆绑(实测 dev).
|
|
219
|
-
const
|
|
262
|
+
const idempotencyKey = normalizedIdempotencyKey;
|
|
263
|
+
const messageId = idempotencyKey !== undefined
|
|
264
|
+
? stablePublishMessageId(idempotencyKey)
|
|
265
|
+
: undefined;
|
|
266
|
+
const body = await this.http.call('POST', '/a2a/publish', gepEnvelope('publish', { assets: bundle }, messageId ? { messageId } : {}));
|
|
220
267
|
return publishRespToReceipt(200, body);
|
|
221
268
|
}
|
|
222
269
|
catch (e) {
|
|
223
|
-
if (e instanceof HubClientError)
|
|
224
|
-
return publishRespToReceipt(e.status, e.body ?? {});
|
|
270
|
+
if (e instanceof HubClientError) {
|
|
271
|
+
return publishRespToReceipt(e.status, e.body ?? {}, e.retryAfterMs);
|
|
272
|
+
}
|
|
225
273
|
throw e; // 5xx/网络 → 重试
|
|
226
274
|
}
|
|
227
275
|
}
|
|
@@ -230,29 +278,35 @@ export class PublicHubCapability {
|
|
|
230
278
|
// /a2a/fetch responses are FULL GEP envelopes (buildResponse('fetch', …)); the rows live at payload.results,
|
|
231
279
|
// NOT at the top level. Reading body.results here always yielded [] — every fetch silently returned nothing.
|
|
232
280
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToFetchWire(query)));
|
|
233
|
-
return (body
|
|
281
|
+
return assetsFromBody(body);
|
|
234
282
|
}
|
|
235
283
|
async fetchAssetById(assetId) {
|
|
236
284
|
const id = assetId.trim();
|
|
237
285
|
if (!id)
|
|
238
286
|
return null;
|
|
239
287
|
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', { asset_ids: [id] }));
|
|
240
|
-
return assetsFromBody(body).find((asset) =>
|
|
288
|
+
return assetsFromBody(body).find((asset) => fetchResultMatchesId(asset, id)) ?? null;
|
|
241
289
|
}
|
|
242
290
|
/**
|
|
243
291
|
* #69: search != fetch. Free-text is the hub's vector endpoint (GET /a2a/assets/semantic-search?q=);
|
|
244
|
-
*
|
|
292
|
+
* signal/id queries use the Hub's free search-only phase on /a2a/fetch. /a2a/fetch does NOT do semantic,
|
|
293
|
+
* so text must not go there and paid/full fetch must remain an explicit follow-up.
|
|
245
294
|
*/
|
|
246
295
|
async search(query) {
|
|
247
296
|
if (query.text && query.text.trim()) {
|
|
248
297
|
// GET /a2a/assets/semantic-search returns a FLAT object keyed `assets` (no GEP envelope), plus a
|
|
249
|
-
// `search_status` (found / degraded(retryable) / low_confidence_only / no_match).
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
298
|
+
// `search_status` (found / degraded(retryable) / low_confidence_only / no_match). Only an explicit
|
|
299
|
+
// no_match is a verified empty result; degraded or malformed 200 responses must not trigger ATP spend.
|
|
300
|
+
const body = await this.http.call('GET', '/a2a/assets/semantic-search', undefined, {
|
|
301
|
+
q: query.text,
|
|
302
|
+
...(query.kind !== undefined ? { type: query.kind } : {}),
|
|
303
|
+
...(query.domain !== undefined ? { domain: query.domain } : {}),
|
|
304
|
+
...(query.limit !== undefined ? { limit: query.limit } : {}),
|
|
305
|
+
});
|
|
306
|
+
return semanticSearchAssets(body);
|
|
254
307
|
}
|
|
255
|
-
|
|
308
|
+
const body = await this.http.call('POST', '/a2a/fetch', gepEnvelope('fetch', searchQueryToSearchOnlyWire(query)));
|
|
309
|
+
return signalSearchAssets(body);
|
|
256
310
|
}
|
|
257
311
|
agentDirectory = {
|
|
258
312
|
search: async (request) => {
|
|
@@ -402,6 +456,52 @@ export class PublicHubCapability {
|
|
|
402
456
|
return { recorded: false, reason: e instanceof Error ? e.message : String(e) };
|
|
403
457
|
}
|
|
404
458
|
}
|
|
459
|
+
async listLearningAssets(options = {}) {
|
|
460
|
+
const limit = normalizeLearningAssetLimit(options.limit);
|
|
461
|
+
if (!this.opts.senderId()?.trim())
|
|
462
|
+
return { assets: [], limit, reason: 'sender_id_required' };
|
|
463
|
+
try {
|
|
464
|
+
const body = await this.http.call('GET', '/a2a/learning-assets', undefined, learningAssetListQuery(options, limit));
|
|
465
|
+
const payload = asRecord(body['payload']) ?? body;
|
|
466
|
+
return {
|
|
467
|
+
assets: learningAssetsFromPayload(payload),
|
|
468
|
+
limit: numberField(payload, 'limit') ?? limit,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
catch (e) {
|
|
472
|
+
return { assets: [], limit, reason: failureReason(e) };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
async recordLearningAssetUsage(report) {
|
|
476
|
+
if (!this.opts.senderId()?.trim())
|
|
477
|
+
return { recorded: false, reason: 'sender_id_required', results: [] };
|
|
478
|
+
const sourceEventId = trimStringField(report.sourceEventId, 160);
|
|
479
|
+
if (!sourceEventId)
|
|
480
|
+
return { recorded: false, reason: 'source_event_id_required', results: [] };
|
|
481
|
+
const assetIds = normalizeLearningAssetIds(report.usedAssetIds && report.usedAssetIds.length > 0 ? report.usedAssetIds : [report.assetId]);
|
|
482
|
+
if (assetIds.length === 0)
|
|
483
|
+
return { recorded: false, reason: 'asset_id_required', results: [] };
|
|
484
|
+
const outcome = normalizeLearningAssetOutcome(report.outcome);
|
|
485
|
+
if (!outcome)
|
|
486
|
+
return { recorded: false, reason: 'invalid_learning_asset_outcome', results: [] };
|
|
487
|
+
const score = optionalLearningAssetScore(report.score);
|
|
488
|
+
if ('reason' in score)
|
|
489
|
+
return { recorded: false, reason: score.reason, results: [] };
|
|
490
|
+
const reason = trimStringField(report.reason, 2_000);
|
|
491
|
+
try {
|
|
492
|
+
const body = await this.http.call('POST', '/a2a/learning-assets/usage', {
|
|
493
|
+
...(assetIds.length === 1 ? { asset_id: assetIds[0] } : { used_asset_ids: assetIds }),
|
|
494
|
+
outcome,
|
|
495
|
+
source_event_id: sourceEventId,
|
|
496
|
+
...(score.value !== undefined ? { score: score.value } : {}),
|
|
497
|
+
...(reason ? { reason } : {}),
|
|
498
|
+
});
|
|
499
|
+
return learningAssetUsageReceiptFromBody(body);
|
|
500
|
+
}
|
|
501
|
+
catch (e) {
|
|
502
|
+
return { recorded: false, reason: failureReason(e), results: [] };
|
|
503
|
+
}
|
|
504
|
+
}
|
|
405
505
|
/**
|
|
406
506
|
* Pre-publish dry-run (POST /a2a/validate). The hub runs the same hub-side quality +
|
|
407
507
|
* content-safety gate as publish but stores nothing and charges no credits. This adapter is
|
|
@@ -457,14 +557,14 @@ export class PublicHubCapability {
|
|
|
457
557
|
...(request.pricePerExecution !== undefined ? { price_per_execution: request.pricePerExecution } : {}),
|
|
458
558
|
...(request.currency ? { currency: request.currency } : {}),
|
|
459
559
|
...(request.maxConcurrent !== undefined ? { max_concurrent: request.maxConcurrent } : {}),
|
|
460
|
-
});
|
|
560
|
+
}, undefined, request.idempotencyKey ? { 'idempotency-key': request.idempotencyKey } : undefined);
|
|
461
561
|
return recipeReceiptFromBody(body);
|
|
462
562
|
}
|
|
463
|
-
async publishRecipe(recipeId) {
|
|
563
|
+
async publishRecipe(recipeId, options) {
|
|
464
564
|
if (isHubDryRunEnabled())
|
|
465
565
|
return dryRunRecipeReceipt('publish_recipe', recipeId);
|
|
466
566
|
const sender = this.opts.senderId();
|
|
467
|
-
const body = await this.http.call('POST', `/a2a/recipe/${encodeURIComponent(recipeId)}/publish`, { ...(sender ? { node_id: sender } : {}) });
|
|
567
|
+
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
568
|
return recipeReceiptFromBody(body);
|
|
469
569
|
}
|
|
470
570
|
async getRecipe(recipeId) {
|
|
@@ -555,7 +655,10 @@ export class PublicHubCapability {
|
|
|
555
655
|
const body = await this.http.call('POST', '/a2a/events/poll', gepEnvelope('events_poll', { timeout_ms: 1000 }));
|
|
556
656
|
for (const e of body.events ?? []) {
|
|
557
657
|
if (String(e['type']).startsWith('task_')) {
|
|
558
|
-
|
|
658
|
+
const payload = asRecord(e['payload']);
|
|
659
|
+
const wireTaskId = payload?.['task_id'] ?? payload?.['taskId'];
|
|
660
|
+
const taskId = typeof wireTaskId === 'string' && wireTaskId.length > 0 ? wireTaskId : String(e['id']);
|
|
661
|
+
yield { taskId, type: String(e['type']), payload: e['payload'], priority: e['priority'] ?? 'medium', createdAt: Date.parse(String(e['created_at'] ?? '')) || 0 };
|
|
559
662
|
}
|
|
560
663
|
}
|
|
561
664
|
}
|
|
@@ -655,6 +758,44 @@ export class PublicHubCapability {
|
|
|
655
758
|
function asRecord(value) {
|
|
656
759
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
657
760
|
}
|
|
761
|
+
function searchAssets(value, source) {
|
|
762
|
+
if (!Array.isArray(value))
|
|
763
|
+
throw new Error(`${source}_results_invalid`);
|
|
764
|
+
return value.map((candidate) => {
|
|
765
|
+
const record = asRecord(candidate);
|
|
766
|
+
if (!record)
|
|
767
|
+
throw new Error(`${source}_asset_invalid`);
|
|
768
|
+
const asset = unwrapFetchDeliveryRow(record);
|
|
769
|
+
const assetId = stringField(asset, 'asset_id') ?? stringField(asset, 'assetId');
|
|
770
|
+
if (!assetId)
|
|
771
|
+
throw new Error(`${source}_asset_invalid`);
|
|
772
|
+
return asset;
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
function semanticSearchAssets(body) {
|
|
776
|
+
const status = stringField(body, 'search_status');
|
|
777
|
+
if (status === 'degraded' || body['retryable'] === true)
|
|
778
|
+
throw new Error('semantic_search_degraded');
|
|
779
|
+
const assets = searchAssets(body['assets'], 'semantic_search');
|
|
780
|
+
if (status === 'no_match') {
|
|
781
|
+
if (assets.length !== 0)
|
|
782
|
+
throw new Error('semantic_search_status_invalid');
|
|
783
|
+
return assets;
|
|
784
|
+
}
|
|
785
|
+
if (status === 'found' || status === 'low_confidence_only') {
|
|
786
|
+
if (assets.length === 0)
|
|
787
|
+
throw new Error('semantic_search_status_invalid');
|
|
788
|
+
return assets;
|
|
789
|
+
}
|
|
790
|
+
// Older successful Hub responses are usable only when they carry a concrete candidate.
|
|
791
|
+
if (status === undefined && assets.length > 0)
|
|
792
|
+
return assets;
|
|
793
|
+
throw new Error('semantic_search_status_invalid');
|
|
794
|
+
}
|
|
795
|
+
function signalSearchAssets(body) {
|
|
796
|
+
const payload = asRecord(body['payload']);
|
|
797
|
+
return searchAssets(payload?.['results'], 'signal_search');
|
|
798
|
+
}
|
|
658
799
|
function recipeStepToWire(step) {
|
|
659
800
|
return {
|
|
660
801
|
asset_id: step.assetId,
|
|
@@ -761,12 +902,115 @@ function accountAssetsFromPayload(payload) {
|
|
|
761
902
|
for (const candidate of candidates) {
|
|
762
903
|
if (!Array.isArray(candidate))
|
|
763
904
|
continue;
|
|
764
|
-
return candidate
|
|
905
|
+
return candidate
|
|
906
|
+
.filter((asset) => Boolean(asset && typeof asset === 'object' && !Array.isArray(asset)))
|
|
907
|
+
.map(unwrapFetchDeliveryRow);
|
|
908
|
+
}
|
|
909
|
+
return [];
|
|
910
|
+
}
|
|
911
|
+
function learningAssetsFromPayload(payload) {
|
|
912
|
+
const candidates = [
|
|
913
|
+
payload['assets'],
|
|
914
|
+
payload['results'],
|
|
915
|
+
payload['items'],
|
|
916
|
+
];
|
|
917
|
+
for (const candidate of candidates) {
|
|
918
|
+
if (!Array.isArray(candidate))
|
|
919
|
+
continue;
|
|
920
|
+
return candidate.filter((asset) => isLearningAssetRecord(asset));
|
|
765
921
|
}
|
|
766
922
|
return [];
|
|
767
923
|
}
|
|
768
|
-
function
|
|
769
|
-
|
|
924
|
+
function isLearningAssetRecord(value) {
|
|
925
|
+
const record = asRecord(value);
|
|
926
|
+
return Boolean(record && typeof record['asset_id'] === 'string' && typeof record['type'] === 'string');
|
|
927
|
+
}
|
|
928
|
+
function normalizeLearningAssetLimit(value) {
|
|
929
|
+
if (!Number.isFinite(value))
|
|
930
|
+
return 20;
|
|
931
|
+
return Math.min(100, Math.max(1, Math.floor(value)));
|
|
932
|
+
}
|
|
933
|
+
function learningAssetListQuery(options, limit) {
|
|
934
|
+
const status = normalizeLearningAssetStatusParam(options.status);
|
|
935
|
+
const query = {
|
|
936
|
+
limit,
|
|
937
|
+
runtime: options.includeExpired === true ? undefined : 'true',
|
|
938
|
+
include_expired: options.includeExpired === true ? 'true' : undefined,
|
|
939
|
+
include_payload: options.includePayload === true ? 'true' : undefined,
|
|
940
|
+
...(options.type ? { type: options.type } : {}),
|
|
941
|
+
...(status ? { status } : options.includeExpired === true ? { status: 'active' } : {}),
|
|
942
|
+
};
|
|
943
|
+
const scope = compactLearningAssetParam(options.scope);
|
|
944
|
+
if (scope)
|
|
945
|
+
query['scope'] = scope;
|
|
946
|
+
return query;
|
|
947
|
+
}
|
|
948
|
+
function normalizeLearningAssetStatusParam(status) {
|
|
949
|
+
if (Array.isArray(status))
|
|
950
|
+
return compactLearningAssetParam(status);
|
|
951
|
+
return typeof status === 'string' && status.trim() ? status.trim() : undefined;
|
|
952
|
+
}
|
|
953
|
+
function compactLearningAssetParam(values) {
|
|
954
|
+
if (!values)
|
|
955
|
+
return undefined;
|
|
956
|
+
const out = [...new Set(values.map((value) => String(value).trim()).filter(Boolean))];
|
|
957
|
+
return out.length > 0 ? out.join(',') : undefined;
|
|
958
|
+
}
|
|
959
|
+
function trimStringField(value, maxLen) {
|
|
960
|
+
return typeof value === 'string' && value.trim() ? value.trim().slice(0, maxLen) : undefined;
|
|
961
|
+
}
|
|
962
|
+
function normalizeLearningAssetIds(values) {
|
|
963
|
+
const out = [];
|
|
964
|
+
const seen = new Set();
|
|
965
|
+
for (const value of values) {
|
|
966
|
+
if (typeof value !== 'string')
|
|
967
|
+
continue;
|
|
968
|
+
const trimmed = value.trim();
|
|
969
|
+
if (!trimmed || trimmed.length > LEARNING_ASSET_ID_MAX_LEN || seen.has(trimmed))
|
|
970
|
+
continue;
|
|
971
|
+
seen.add(trimmed);
|
|
972
|
+
out.push(trimmed);
|
|
973
|
+
if (out.length >= LEARNING_ASSET_IDS_MAX)
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
return out;
|
|
977
|
+
}
|
|
978
|
+
function normalizeLearningAssetOutcome(value) {
|
|
979
|
+
if (value === 'success' || value === 'failed' || value === 'mismatched' || value === 'stale' || value === 'unsafe')
|
|
980
|
+
return value;
|
|
981
|
+
return undefined;
|
|
982
|
+
}
|
|
983
|
+
function optionalLearningAssetScore(value) {
|
|
984
|
+
if (value === undefined)
|
|
985
|
+
return { value: undefined };
|
|
986
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 1)
|
|
987
|
+
return { reason: 'invalid_score' };
|
|
988
|
+
return { value };
|
|
989
|
+
}
|
|
990
|
+
function learningAssetUsageReceiptFromBody(body) {
|
|
991
|
+
const payload = asRecord(body['payload']) ?? body;
|
|
992
|
+
const rows = Array.isArray(payload['results'])
|
|
993
|
+
? payload['results'].filter((row) => Boolean(asRecord(row)))
|
|
994
|
+
: [];
|
|
995
|
+
const reason = stringField(payload, 'reason') ?? stringField(payload, 'error');
|
|
996
|
+
return {
|
|
997
|
+
recorded: rows.length > 0 && rows.some((row) => row.recorded === true),
|
|
998
|
+
...(reason ? { reason } : {}),
|
|
999
|
+
results: rows,
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
function failureReason(error) {
|
|
1003
|
+
if (error instanceof HubClientError) {
|
|
1004
|
+
const body = asRecord(error.body) ?? {};
|
|
1005
|
+
const payload = asRecord(body['payload']) ?? body;
|
|
1006
|
+
return stringField(payload, 'reason') ?? stringField(payload, 'error') ?? `hub ${error.status}`;
|
|
1007
|
+
}
|
|
1008
|
+
return error instanceof Error ? error.message : String(error);
|
|
1009
|
+
}
|
|
1010
|
+
function fetchResultMatchesId(asset, requestedId) {
|
|
1011
|
+
if (assetMatchesId(asset, requestedId))
|
|
1012
|
+
return true;
|
|
1013
|
+
return !requestedId.startsWith('sha256:') && Boolean(asset && stringField(asset, 'id') === requestedId);
|
|
770
1014
|
}
|
|
771
1015
|
function stringField(value, key) {
|
|
772
1016
|
return typeof value[key] === 'string' && value[key].length > 0 ? value[key] : undefined;
|
|
@@ -840,14 +1084,39 @@ function mailboxPushResultFromBody(body, events) {
|
|
|
840
1084
|
if (results.length === 0) {
|
|
841
1085
|
return { outcomes: events.map((event) => ({ id: event.id, status: 'accepted' })) };
|
|
842
1086
|
}
|
|
1087
|
+
const resultIds = results.map(mailboxPushResultId);
|
|
1088
|
+
const hasCompletePositions = results.length === events.length;
|
|
843
1089
|
return {
|
|
844
|
-
outcomes: events.map((event, index) =>
|
|
1090
|
+
outcomes: events.map((event, index) => {
|
|
1091
|
+
const matches = results.filter((_, resultIndex) => resultIds[resultIndex] === event.id);
|
|
1092
|
+
if (matches.length === 1)
|
|
1093
|
+
return mailboxPushOutcomeFromRow(event.id, matches[0]);
|
|
1094
|
+
const positionalMatch = matches.length === 0
|
|
1095
|
+
&& hasCompletePositions
|
|
1096
|
+
&& resultIds[index] === undefined
|
|
1097
|
+
? results[index]
|
|
1098
|
+
: undefined;
|
|
1099
|
+
return mailboxPushOutcomeFromRow(event.id, positionalMatch);
|
|
1100
|
+
}),
|
|
845
1101
|
};
|
|
846
1102
|
}
|
|
847
|
-
function
|
|
848
|
-
const
|
|
849
|
-
if (
|
|
850
|
-
return
|
|
1103
|
+
function mailboxPushResultId(row) {
|
|
1104
|
+
const value = row['id'] ?? row['message_id'];
|
|
1105
|
+
if (typeof value !== 'string' && typeof value !== 'number')
|
|
1106
|
+
return undefined;
|
|
1107
|
+
const id = String(value);
|
|
1108
|
+
return id.length > 0 ? id : undefined;
|
|
1109
|
+
}
|
|
1110
|
+
function mailboxPushOutcomeFromRow(eventId, match) {
|
|
1111
|
+
if (!match) {
|
|
1112
|
+
return {
|
|
1113
|
+
id: eventId,
|
|
1114
|
+
status: 'failed',
|
|
1115
|
+
reason: 'mailbox_response_incomplete',
|
|
1116
|
+
retryable: true,
|
|
1117
|
+
terminal: false,
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
851
1120
|
const reason = mailboxPushFailureReason(match);
|
|
852
1121
|
if (!reason)
|
|
853
1122
|
return { id: eventId, status: 'accepted', raw: match };
|
|
@@ -900,9 +1169,12 @@ function hubErrorStatus(err) {
|
|
|
900
1169
|
}
|
|
901
1170
|
return undefined;
|
|
902
1171
|
}
|
|
903
|
-
// Retry-After
|
|
904
|
-
//
|
|
1172
|
+
// Prefer the standardized Retry-After header carried by HubClientError, then
|
|
1173
|
+
// preserve the existing JSON body hints as a compatibility fallback.
|
|
905
1174
|
function clientErrorRetryAfterMs(err) {
|
|
1175
|
+
if (err instanceof HubClientError && typeof err.retryAfterMs === 'number' && Number.isFinite(err.retryAfterMs)) {
|
|
1176
|
+
return err.retryAfterMs;
|
|
1177
|
+
}
|
|
906
1178
|
const body = err instanceof HubClientError ? asRecord(err.body) : undefined;
|
|
907
1179
|
if (!body)
|
|
908
1180
|
return undefined;
|
|
@@ -1046,6 +1318,10 @@ function heartbeatResultFromBody(httpStatus, body) {
|
|
|
1046
1318
|
const details = payload['details'] ?? body['details'];
|
|
1047
1319
|
const ack = asRecord(payload['last_update_ack']);
|
|
1048
1320
|
const forceUpdate = forceUpdateFromRecord(asRecord(payload['force_update']));
|
|
1321
|
+
const rawCapabilityGaps = payload['capability_gaps'] ?? payload['capabilityGaps'];
|
|
1322
|
+
const capabilityGaps = Array.isArray(rawCapabilityGaps)
|
|
1323
|
+
? signals.normalizeCapabilityGaps(rawCapabilityGaps)
|
|
1324
|
+
: undefined;
|
|
1049
1325
|
const ok = httpStatus >= 200
|
|
1050
1326
|
&& httpStatus < 300
|
|
1051
1327
|
&& payload['ok'] !== false
|
|
@@ -1063,6 +1339,7 @@ function heartbeatResultFromBody(httpStatus, body) {
|
|
|
1063
1339
|
...(typeof ack['reason'] === 'string' ? { reason: ack['reason'] } : {}),
|
|
1064
1340
|
} } : {}),
|
|
1065
1341
|
...(forceUpdate ? { forceUpdate } : {}),
|
|
1342
|
+
...(capabilityGaps !== undefined ? { capabilityGaps } : {}),
|
|
1066
1343
|
};
|
|
1067
1344
|
}
|
|
1068
1345
|
function forceUpdateFromRecord(value) {
|