@evomap/evolver-adapter-public 2.0.0-beta.2 → 2.0.0-beta.22
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 +90 -3
- package/dist/auth/credentialStore.js +1096 -10
- package/dist/auth/legacyShim.d.ts +2 -2
- package/dist/auth/legacyShim.js +2 -2
- 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 +35 -9
- package/dist/hubCapability.js +593 -54
- package/dist/hubFetch.d.ts +47 -14
- package/dist/hubFetch.js +374 -79
- package/dist/hubReuse.d.ts +41 -0
- package/dist/hubReuse.js +328 -33
- 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 +48 -0
- package/dist/learningPacketSink.js +194 -0
- package/dist/wireMap.d.ts +3 -1
- package/dist/wireMap.js +29 -3
- package/package.json +9 -2
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { assertHubUrlSecure, isHubUnreachableError } from './hubFetch.js';
|
|
2
|
+
import { learningOpsAuthHeaders } from './learningPacketSink.js';
|
|
3
|
+
/** Hub appendLearningFeedbackSchema closed enums (evomap-hub src/schemas/learningOps.js). */
|
|
4
|
+
export const LEARNING_FEEDBACK_TYPES = ['outcome', 'rating', 'correction', 'governance', 'note'];
|
|
5
|
+
export const LEARNING_FEEDBACK_DECISIONS = [
|
|
6
|
+
'accepted', 'rejected', 'needs_redaction', 'not_training_eligible', 'training_candidate', 'note',
|
|
7
|
+
];
|
|
8
|
+
function pickString(record, key) {
|
|
9
|
+
const value = record[key];
|
|
10
|
+
if (value === null)
|
|
11
|
+
return null;
|
|
12
|
+
return typeof value === 'string' ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
async function failureReason(res) {
|
|
15
|
+
const text = await res.text().catch(() => '');
|
|
16
|
+
return `hub ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}`;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Feedback append + packet governance read-back against the hub Learning Ops API. Best-effort by the
|
|
20
|
+
* same contract as HubLearningPacketSink: this is observability/ops tooling, so every failure —
|
|
21
|
+
* network, auth, 4xx/5xx, unparseable body — returns { ok:false, reason } and never throws.
|
|
22
|
+
*/
|
|
23
|
+
export class HubLearningPacketFeedbackClient {
|
|
24
|
+
opts;
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
}
|
|
28
|
+
async submitFeedback(packetId, feedback) {
|
|
29
|
+
try {
|
|
30
|
+
const path = `/api/learning-packets/${encodeURIComponent(packetId)}/feedback`;
|
|
31
|
+
const url = `${this.opts.baseUrl}${path}`;
|
|
32
|
+
assertHubUrlSecure(url);
|
|
33
|
+
const headers = await learningOpsAuthHeaders(this.opts.auth, 'POST', path);
|
|
34
|
+
// Exactly the appendLearningFeedbackSchema fields (strict zod): optional keys are omitted, not nulled.
|
|
35
|
+
const res = await this.opts.fetchFn(url, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers,
|
|
38
|
+
redirect: 'manual',
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
decision: feedback.decision,
|
|
41
|
+
...(feedback.feedbackType !== undefined ? { feedbackType: feedback.feedbackType } : {}),
|
|
42
|
+
...(feedback.rating !== undefined ? { rating: feedback.rating } : {}),
|
|
43
|
+
...(feedback.scores !== undefined ? { scores: feedback.scores } : {}),
|
|
44
|
+
...(feedback.rationale !== undefined ? { rationale: feedback.rationale } : {}),
|
|
45
|
+
...(feedback.verifier !== undefined ? { verifier: feedback.verifier } : {}),
|
|
46
|
+
...(feedback.failureCategory !== undefined ? { failureCategory: feedback.failureCategory } : {}),
|
|
47
|
+
...(feedback.traceEventId !== undefined ? { traceEventId: feedback.traceEventId } : {}),
|
|
48
|
+
...(feedback.actorNodeId !== undefined ? { actorNodeId: feedback.actorNodeId } : {}),
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
if (res.status === 201) {
|
|
52
|
+
const body = await res.json().catch(() => null);
|
|
53
|
+
const row = body && typeof body === 'object' ? body.feedback : undefined;
|
|
54
|
+
return { ok: true, ...(typeof row?.id === 'string' ? { feedbackId: row.id } : {}) };
|
|
55
|
+
}
|
|
56
|
+
return { ok: false, reason: await failureReason(res) };
|
|
57
|
+
}
|
|
58
|
+
catch (e) {
|
|
59
|
+
if (isHubUnreachableError(e))
|
|
60
|
+
return { ok: false, reason: 'hub_unreachable' };
|
|
61
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async getPacket(packetId) {
|
|
65
|
+
try {
|
|
66
|
+
const path = `/api/learning-packets/${encodeURIComponent(packetId)}`;
|
|
67
|
+
const url = `${this.opts.baseUrl}${path}`;
|
|
68
|
+
assertHubUrlSecure(url);
|
|
69
|
+
const headers = await learningOpsAuthHeaders(this.opts.auth, 'GET', path);
|
|
70
|
+
const res = await this.opts.fetchFn(url, { method: 'GET', headers, redirect: 'manual' });
|
|
71
|
+
if (res.status !== 200)
|
|
72
|
+
return { ok: false, reason: await failureReason(res) };
|
|
73
|
+
const body = await res.json().catch(() => null);
|
|
74
|
+
const packet = body && typeof body === 'object' ? body.packet : undefined;
|
|
75
|
+
if (!packet || typeof packet !== 'object' || Array.isArray(packet)) {
|
|
76
|
+
return { ok: false, reason: 'hub 200: response missing packet object' };
|
|
77
|
+
}
|
|
78
|
+
const record = packet;
|
|
79
|
+
if (typeof record['id'] !== 'string' || record['id'].length === 0) {
|
|
80
|
+
return { ok: false, reason: 'hub 200: packet missing id' };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
ok: true,
|
|
84
|
+
packet: {
|
|
85
|
+
id: record['id'],
|
|
86
|
+
...(pickString(record, 'status') !== undefined && pickString(record, 'status') !== null ? { status: record['status'] } : {}),
|
|
87
|
+
...(pickString(record, 'outcomeStatus') !== undefined ? { outcomeStatus: pickString(record, 'outcomeStatus') } : {}),
|
|
88
|
+
...(pickString(record, 'verifier') !== undefined ? { verifier: pickString(record, 'verifier') } : {}),
|
|
89
|
+
...(pickString(record, 'trainingEligibilityStatus') !== undefined ? { trainingEligibilityStatus: pickString(record, 'trainingEligibilityStatus') } : {}),
|
|
90
|
+
...(pickString(record, 'governanceStatus') !== undefined ? { governanceStatus: pickString(record, 'governanceStatus') } : {}),
|
|
91
|
+
...(typeof record['trainingEligible'] === 'boolean' ? { trainingEligible: record['trainingEligible'] } : {}),
|
|
92
|
+
...(pickString(record, 'consentStatus') !== undefined ? { consentStatus: pickString(record, 'consentStatus') } : {}),
|
|
93
|
+
...(pickString(record, 'redactionStatus') !== undefined ? { redactionStatus: pickString(record, 'redactionStatus') } : {}),
|
|
94
|
+
...(pickString(record, 'retentionPolicy') !== undefined ? { retentionPolicy: pickString(record, 'retentionPolicy') } : {}),
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
if (isHubUnreachableError(e))
|
|
100
|
+
return { ok: false, reason: 'hub_unreachable' };
|
|
101
|
+
return { ok: false, reason: e instanceof Error ? e.message : String(e) };
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
/**
|
|
13
|
+
* Deterministic content hash over the draft body (hub contentHash column, dedup aid).
|
|
14
|
+
*
|
|
15
|
+
* Bare 64-hex, NOT `sha256:`-prefixed: the hub column is VarChar(64), so a prefixed
|
|
16
|
+
* digest is 71 chars and every upload failed with a Prisma "value too long" 500. The
|
|
17
|
+
* hub schema now rejects over-64 at validation, which would make it a 400 instead —
|
|
18
|
+
* either way the algorithm is fixed at sha256 by this contract, so the prefix carried
|
|
19
|
+
* no information.
|
|
20
|
+
*/
|
|
21
|
+
export declare function learningPacketContentHash(draft: trace.LearningPacketDraft): string;
|
|
22
|
+
/**
|
|
23
|
+
* Auth headers for the strict learning-packets routes (requireAuth reads Authorization only).
|
|
24
|
+
* A legacy body credential (bodyFields.node_secret) is promoted to Bearer — the strict schemas
|
|
25
|
+
* reject extra body fields, so credentials must never ride in the body here.
|
|
26
|
+
*/
|
|
27
|
+
export declare function learningOpsAuthHeaders(auth: hub.AuthProvider, method: string, path: string): Promise<Record<string, string>>;
|
|
28
|
+
/** Map a core draft to the hub createLearningPacket body. Exported for tests and for the private adapter to reuse. */
|
|
29
|
+
export declare function learningPacketWireBody(draft: trace.LearningPacketDraft, nodeId?: string): Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* LearningPacketSink implementation against the public hub Learning Ops ingest API. Best-effort by
|
|
32
|
+
* contract: every failure returns { accepted: false, reason } (never throws) — the runtime treats packet
|
|
33
|
+
* delivery as observability, so a hub outage must never affect a task verdict. A 409 duplicate_source is
|
|
34
|
+
* reported as accepted (the packet is already there; the idempotency key did its job).
|
|
35
|
+
*/
|
|
36
|
+
export declare class HubLearningPacketSink implements trace.LearningPacketSink {
|
|
37
|
+
private readonly opts;
|
|
38
|
+
constructor(opts: HubLearningPacketSinkOptions);
|
|
39
|
+
submit(draft: trace.LearningPacketDraft): Promise<trace.LearningPacketSubmitResult>;
|
|
40
|
+
}
|
|
41
|
+
/** Fan-out: always deliver to `primary` (local file record), then best-effort to `secondary` (hub upload).
|
|
42
|
+
* The composite result reflects the PRIMARY sink — the local record is the durability guarantee. */
|
|
43
|
+
export declare class TeeLearningPacketSink implements trace.LearningPacketSink {
|
|
44
|
+
private readonly primary;
|
|
45
|
+
private readonly secondary;
|
|
46
|
+
constructor(primary: trace.LearningPacketSink, secondary: trace.LearningPacketSink);
|
|
47
|
+
submit(draft: trace.LearningPacketDraft): Promise<trace.LearningPacketSubmitResult>;
|
|
48
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
/**
|
|
23
|
+
* Map the runtime's outcome onto the hub OUTCOME_STATUSES enum, tiered by whether an
|
|
24
|
+
* external verifier actually adjudicated the run.
|
|
25
|
+
*
|
|
26
|
+
* A verified run gets a definite verdict (`succeeded` / `failed`). An unverified one
|
|
27
|
+
* gets `partially_succeeded` -- deliberately NOT `succeeded`, and no longer omitted:
|
|
28
|
+
*
|
|
29
|
+
* - Omitting it (the previous behaviour) threw the run away. The packet reached the
|
|
30
|
+
* hub with no outcome at all, which is indistinguishable from a run nobody looked
|
|
31
|
+
* at, so a consumer could not tell "we don't know" from "not recorded".
|
|
32
|
+
* - Calling it `succeeded` would be worse: the runtime only knows the turn loop
|
|
33
|
+
* ended without crashing, which is not evidence the task was done correctly.
|
|
34
|
+
* Training on that teaches format imitation.
|
|
35
|
+
*
|
|
36
|
+
* `partially_succeeded` says exactly what is true -- it ran to completion and nobody
|
|
37
|
+
* checked the result -- and pairs with `verifier` being absent, so a consumer filters
|
|
38
|
+
* on the verifier rather than having to infer trust from the status. Darwin's training
|
|
39
|
+
* path takes only rows with a real verifier; see docs/rsi-stage1-plan.md.
|
|
40
|
+
* @param status Runtime-side outcome.
|
|
41
|
+
* @param verified True when an external verifier ran (evaluation.placeholder === false).
|
|
42
|
+
* @returns A hub OUTCOME_STATUSES value.
|
|
43
|
+
*/
|
|
44
|
+
function outcomeStatusFor(status, verified) {
|
|
45
|
+
if (status === 'failed')
|
|
46
|
+
return 'failed';
|
|
47
|
+
if (status === 'success' && verified)
|
|
48
|
+
return 'succeeded';
|
|
49
|
+
// Ran to completion, unadjudicated -- or the runtime itself is unsure.
|
|
50
|
+
return 'partially_succeeded';
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Deterministic content hash over the draft body (hub contentHash column, dedup aid).
|
|
54
|
+
*
|
|
55
|
+
* Bare 64-hex, NOT `sha256:`-prefixed: the hub column is VarChar(64), so a prefixed
|
|
56
|
+
* digest is 71 chars and every upload failed with a Prisma "value too long" 500. The
|
|
57
|
+
* hub schema now rejects over-64 at validation, which would make it a 400 instead —
|
|
58
|
+
* either way the algorithm is fixed at sha256 by this contract, so the prefix carried
|
|
59
|
+
* no information.
|
|
60
|
+
*/
|
|
61
|
+
export function learningPacketContentHash(draft) {
|
|
62
|
+
return createHash('sha256').update(JSON.stringify(draft)).digest('hex');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Auth headers for the strict learning-packets routes (requireAuth reads Authorization only).
|
|
66
|
+
* A legacy body credential (bodyFields.node_secret) is promoted to Bearer — the strict schemas
|
|
67
|
+
* reject extra body fields, so credentials must never ride in the body here.
|
|
68
|
+
*/
|
|
69
|
+
export async function learningOpsAuthHeaders(auth, method, path) {
|
|
70
|
+
const signed = await auth.authenticate({ method, path });
|
|
71
|
+
const headers = { 'content-type': 'application/json', ...(signed.headers ?? {}) };
|
|
72
|
+
const bodySecret = signed.bodyFields?.['node_secret'];
|
|
73
|
+
if (headers['authorization'] === undefined && bodySecret !== undefined)
|
|
74
|
+
headers['authorization'] = `Bearer ${String(bodySecret)}`;
|
|
75
|
+
return headers;
|
|
76
|
+
}
|
|
77
|
+
/** Map a core draft to the hub createLearningPacket body. Exported for tests and for the private adapter to reuse. */
|
|
78
|
+
export function learningPacketWireBody(draft, nodeId) {
|
|
79
|
+
const truncated = draft.trajectory.length > HUB_TRACE_EVENTS_MAX;
|
|
80
|
+
const events = draft.trajectory.slice(0, HUB_TRACE_EVENTS_MAX);
|
|
81
|
+
const outcomeStatus = outcomeStatusFor(draft.evaluation.outcomeStatus, draft.evaluation.placeholder === false);
|
|
82
|
+
const failureCategory = failureCategoryFor(draft.evaluation.failureCategory);
|
|
83
|
+
return {
|
|
84
|
+
schemaVersion: draft.schemaVersion,
|
|
85
|
+
status: 'draft',
|
|
86
|
+
sourceRepo: draft.source.repo,
|
|
87
|
+
sourceRun: draft.source.run,
|
|
88
|
+
sourceType: draft.source.type,
|
|
89
|
+
sourceId: draft.source.id,
|
|
90
|
+
// One packet per run: the run id IS the idempotency key, so a retried submit dedups hub-side (409).
|
|
91
|
+
idempotencyKey: `${draft.source.repo}:${draft.source.run}`,
|
|
92
|
+
contentHash: learningPacketContentHash(draft),
|
|
93
|
+
...(nodeId ? { nodeId } : {}),
|
|
94
|
+
outcomeStatus,
|
|
95
|
+
// evaluation fill-in (slice 6): a non-placeholder evaluation carries the runtime's external verifier
|
|
96
|
+
// ('automated_test' is in the hub VERIFIERS enum); passed/score details ride inside payload.evaluation.
|
|
97
|
+
...(draft.evaluation.verifier !== null ? { verifier: draft.evaluation.verifier } : {}),
|
|
98
|
+
...(failureCategory ? { failureCategory } : {}),
|
|
99
|
+
...(draft.task.summary !== null ? { summary: draft.task.summary } : {}),
|
|
100
|
+
payload: {
|
|
101
|
+
task: draft.task,
|
|
102
|
+
context: draft.context,
|
|
103
|
+
artifacts: draft.artifacts,
|
|
104
|
+
evaluation: draft.evaluation,
|
|
105
|
+
governance: draft.governance,
|
|
106
|
+
},
|
|
107
|
+
metadata: {
|
|
108
|
+
...(truncated ? { traceEventsTruncated: true, traceEventsTotal: draft.trajectory.length } : {}),
|
|
109
|
+
...(draft.evaluation.failureCategory !== null ? { runtimeFailureKind: draft.evaluation.failureCategory } : {}),
|
|
110
|
+
},
|
|
111
|
+
redactionStatus: draft.governance.redactionStatus,
|
|
112
|
+
consentStatus: draft.governance.consentStatus,
|
|
113
|
+
retentionPolicy: draft.governance.retentionPolicy,
|
|
114
|
+
traceEvents: events.map((e) => ({
|
|
115
|
+
eventId: e.eventId,
|
|
116
|
+
schemaVersion: e.schemaVersion,
|
|
117
|
+
eventType: e.eventType,
|
|
118
|
+
occurredAt: e.occurredAt,
|
|
119
|
+
traceId: e.traceId,
|
|
120
|
+
...(e.sessionId !== undefined ? { sessionId: e.sessionId } : {}),
|
|
121
|
+
...(e.taskId !== undefined ? { taskId: e.taskId } : {}),
|
|
122
|
+
sequence: e.sequence,
|
|
123
|
+
payload: e.payload,
|
|
124
|
+
metadata: e.metadata,
|
|
125
|
+
})),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* LearningPacketSink implementation against the public hub Learning Ops ingest API. Best-effort by
|
|
130
|
+
* contract: every failure returns { accepted: false, reason } (never throws) — the runtime treats packet
|
|
131
|
+
* delivery as observability, so a hub outage must never affect a task verdict. A 409 duplicate_source is
|
|
132
|
+
* reported as accepted (the packet is already there; the idempotency key did its job).
|
|
133
|
+
*/
|
|
134
|
+
export class HubLearningPacketSink {
|
|
135
|
+
opts;
|
|
136
|
+
constructor(opts) {
|
|
137
|
+
this.opts = opts;
|
|
138
|
+
}
|
|
139
|
+
async submit(draft) {
|
|
140
|
+
try {
|
|
141
|
+
const url = `${this.opts.baseUrl}/api/learning-packets`;
|
|
142
|
+
assertHubUrlSecure(url);
|
|
143
|
+
const headers = await learningOpsAuthHeaders(this.opts.auth, 'POST', '/api/learning-packets');
|
|
144
|
+
const res = await this.opts.fetchFn(url, {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers,
|
|
147
|
+
redirect: 'manual',
|
|
148
|
+
body: JSON.stringify(learningPacketWireBody(draft, this.opts.nodeId?.())),
|
|
149
|
+
});
|
|
150
|
+
// Hub route returns 201 Created. The public website BFF/proxy in front of
|
|
151
|
+
// /api/learning-packets has been observed to surface the same body as 200.
|
|
152
|
+
// Accept both when a packet id is present so a successful write is never
|
|
153
|
+
// reported as hub 200 rejection (which previously made every live upload
|
|
154
|
+
// look failed while the row was already stored).
|
|
155
|
+
if (res.status === 201 || res.status === 200) {
|
|
156
|
+
const body = await res.json().catch(() => null);
|
|
157
|
+
const packet = body && typeof body === 'object' ? body.packet : undefined;
|
|
158
|
+
if (typeof packet?.id === 'string') {
|
|
159
|
+
return { accepted: true, reason: packet.id };
|
|
160
|
+
}
|
|
161
|
+
if (res.status === 201)
|
|
162
|
+
return { accepted: true };
|
|
163
|
+
// Bare 200 without a packet body is not a create success we can claim.
|
|
164
|
+
}
|
|
165
|
+
if (res.status === 409)
|
|
166
|
+
return { accepted: true, reason: 'duplicate_source' };
|
|
167
|
+
const text = await res.text().catch(() => '');
|
|
168
|
+
return { accepted: false, reason: `hub ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}` };
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
if (isHubUnreachableError(e))
|
|
172
|
+
return { accepted: false, reason: 'hub_unreachable' };
|
|
173
|
+
return { accepted: false, reason: e instanceof Error ? e.message : String(e) };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** Fan-out: always deliver to `primary` (local file record), then best-effort to `secondary` (hub upload).
|
|
178
|
+
* The composite result reflects the PRIMARY sink — the local record is the durability guarantee. */
|
|
179
|
+
export class TeeLearningPacketSink {
|
|
180
|
+
primary;
|
|
181
|
+
secondary;
|
|
182
|
+
constructor(primary, secondary) {
|
|
183
|
+
this.primary = primary;
|
|
184
|
+
this.secondary = secondary;
|
|
185
|
+
}
|
|
186
|
+
async submit(draft) {
|
|
187
|
+
const primary = await this.primary.submit(draft);
|
|
188
|
+
try {
|
|
189
|
+
await this.secondary.submit(draft);
|
|
190
|
+
}
|
|
191
|
+
catch { /* secondary is best-effort by contract; sinks should not throw, but never let one break the record */ }
|
|
192
|
+
return primary;
|
|
193
|
+
}
|
|
194
|
+
}
|
package/dist/wireMap.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export declare function inboundToAgentEvent(m: Record<string, unknown>): hub.Age
|
|
|
6
6
|
* payload.signals, #69)。text 不是 fetch 字段(自由文本走 semantic-search 端点, 见 hubCapability.search)。
|
|
7
7
|
*/
|
|
8
8
|
export declare function searchQueryToFetchWire(q: hub.HubQuery): Record<string, unknown>;
|
|
9
|
+
/** Free discovery phase on /a2a/fetch. Keep this separate from the paid/full fetch mapper. */
|
|
10
|
+
export declare function searchQueryToSearchOnlyWire(q: hub.HubQuery): Record<string, unknown>;
|
|
9
11
|
/** core AgentEvent(出站) → 公版 outbound 消息(id+type 必填). */
|
|
10
12
|
export declare function agentEventToOutbound(e: hub.AgentEvent): Record<string, unknown>;
|
|
11
13
|
/** Retry policy for a non-2xx hub status, shared by every money-touching caller (anti-drift, #177). */
|
|
@@ -25,4 +27,4 @@ export type AtpRetryClass = 'permanent' | 'cooldown' | 'recoverable';
|
|
|
25
27
|
*/
|
|
26
28
|
export declare function atpRetryClass(status: number): AtpRetryClass;
|
|
27
29
|
/** /a2a/publish 响应 → PublishReceipt. 200=accepted; 402/4xx=rejected 终态. */
|
|
28
|
-
export declare function publishRespToReceipt(status: number, body: Record<string, unknown
|
|
30
|
+
export declare function publishRespToReceipt(status: number, body: Record<string, unknown>, retryAfterMs?: number): hub.PublishReceipt;
|
package/dist/wireMap.js
CHANGED
|
@@ -24,10 +24,16 @@ export function searchQueryToFetchWire(q) {
|
|
|
24
24
|
out['category'] = q.category;
|
|
25
25
|
if (q.gene)
|
|
26
26
|
out['gene'] = q.gene;
|
|
27
|
+
if (q.domain)
|
|
28
|
+
out['domain'] = q.domain;
|
|
27
29
|
if (q.limit !== undefined)
|
|
28
30
|
out['limit'] = q.limit;
|
|
29
31
|
return out;
|
|
30
32
|
}
|
|
33
|
+
/** Free discovery phase on /a2a/fetch. Keep this separate from the paid/full fetch mapper. */
|
|
34
|
+
export function searchQueryToSearchOnlyWire(q) {
|
|
35
|
+
return { ...searchQueryToFetchWire(q), search_only: true };
|
|
36
|
+
}
|
|
31
37
|
/** core AgentEvent(出站) → 公版 outbound 消息(id+type 必填). */
|
|
32
38
|
export function agentEventToOutbound(e) {
|
|
33
39
|
return {
|
|
@@ -59,14 +65,20 @@ export function atpRetryClass(status) {
|
|
|
59
65
|
return 'recoverable';
|
|
60
66
|
}
|
|
61
67
|
/** /a2a/publish 响应 → PublishReceipt. 200=accepted; 402/4xx=rejected 终态. */
|
|
62
|
-
export function publishRespToReceipt(status, body) {
|
|
68
|
+
export function publishRespToReceipt(status, body, retryAfterMs) {
|
|
63
69
|
const payload = body['payload'] ?? body;
|
|
64
70
|
const assetIds = payload['asset_ids'];
|
|
65
|
-
const
|
|
71
|
+
const targetAssetId = payload['target_asset_id']
|
|
72
|
+
?? body['target_asset_id'];
|
|
73
|
+
const assetId = (status === 409 ? targetAssetId : undefined)
|
|
74
|
+
?? payload['asset_id']
|
|
75
|
+
?? body['asset_id']
|
|
76
|
+
?? assetIds?.[0]
|
|
77
|
+
?? targetAssetId;
|
|
66
78
|
const bundleId = payload['bundle_id'];
|
|
67
79
|
if (status >= 200 && status < 300) {
|
|
68
80
|
const decision = String(payload['decision'] ?? payload['status'] ?? 'accepted');
|
|
69
|
-
const accepted = decision === 'accepted' || decision === 'approved' || decision === 'ok';
|
|
81
|
+
const accepted = decision === 'accept' || decision === 'accepted' || decision === 'approved' || decision === 'ok';
|
|
70
82
|
return {
|
|
71
83
|
receiptId: String(payload['receipt_id'] ?? bundleId ?? payload['id'] ?? assetId ?? 'unknown'),
|
|
72
84
|
status: accepted ? 'accepted' : (decision === 'quarantine' ? 'quarantine' : 'rejected'),
|
|
@@ -80,12 +92,26 @@ export function publishRespToReceipt(status, body) {
|
|
|
80
92
|
// M8-1: 按语义而非纯状态码区分(都终态不重试 = money-safety: 不反复打经济端点).
|
|
81
93
|
// 402=creditShortage(余额不足) / 403=node 失效需 rebind / 409=duplicate / 422=payload 须修 / 429=cooldown.
|
|
82
94
|
const reasonByStatus = { 402: 'credit_shortage', 403: 'node_unauthorized', 409: 'duplicate', 422: 'invalid_payload', 429: 'cooldown' };
|
|
95
|
+
const rejectionCodeByStatus = {
|
|
96
|
+
400: 'invalid_request',
|
|
97
|
+
402: 'credit_shortage',
|
|
98
|
+
403: 'node_unauthorized',
|
|
99
|
+
404: 'not_found',
|
|
100
|
+
409: 'duplicate',
|
|
101
|
+
422: 'invalid_payload',
|
|
102
|
+
429: 'cooldown',
|
|
103
|
+
};
|
|
83
104
|
const receipt = {
|
|
84
105
|
receiptId: String(payload['receipt_id'] ?? 'rejected'),
|
|
85
106
|
status: 'rejected',
|
|
86
107
|
reason: String(payload['reason'] ?? reasonByStatus[status] ?? `hub ${status}`),
|
|
87
108
|
...(assetId ? { assetId } : {}),
|
|
109
|
+
...(assetIds ? { assetIds } : {}),
|
|
88
110
|
terminal: true,
|
|
111
|
+
rejection: {
|
|
112
|
+
code: rejectionCodeByStatus[status] ?? 'hub_rejected',
|
|
113
|
+
...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
|
|
114
|
+
},
|
|
89
115
|
};
|
|
90
116
|
if (status === 402) {
|
|
91
117
|
receipt.economic = {
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evomap/evolver-adapter-public",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.22",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22.13.0 || >=23.4.0"
|
|
8
|
+
},
|
|
6
9
|
"description": "公版 hub 适配器 (积分/治理)",
|
|
7
10
|
"main": "./dist/index.js",
|
|
8
11
|
"types": "./dist/index.d.ts",
|
|
@@ -14,12 +17,16 @@
|
|
|
14
17
|
},
|
|
15
18
|
"dependencies": {
|
|
16
19
|
"@evomap/atp-sdk": "^0.1.0",
|
|
17
|
-
"@evomap/evolver-core": "2.0.0-beta.
|
|
20
|
+
"@evomap/evolver-core": "2.0.0-beta.22",
|
|
18
21
|
"undici": "^6.27.0"
|
|
19
22
|
},
|
|
20
23
|
"optionalDependencies": {
|
|
21
24
|
"@napi-rs/keyring": "^1.1.6"
|
|
22
25
|
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/EvoMap/evolver.git"
|
|
29
|
+
},
|
|
23
30
|
"publishConfig": {
|
|
24
31
|
"access": "public",
|
|
25
32
|
"tag": "v2-beta"
|