@evomap/evolver-core 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.
Files changed (142) hide show
  1. package/dist/algo/candidateAssembly.d.ts +7 -2
  2. package/dist/algo/candidateAssembly.js +81 -13
  3. package/dist/algo/capabilityCandidates.js +10 -0
  4. package/dist/algo/cycleEngine.d.ts +23 -2
  5. package/dist/algo/cycleEngine.js +236 -36
  6. package/dist/algo/exploration.d.ts +7 -0
  7. package/dist/algo/exploration.js +16 -3
  8. package/dist/algo/geneHealth.d.ts +2 -2
  9. package/dist/algo/geneHealth.js +5 -4
  10. package/dist/algo/geneSelection.d.ts +63 -5
  11. package/dist/algo/geneSelection.js +330 -30
  12. package/dist/algo/index.d.ts +2 -0
  13. package/dist/algo/index.js +2 -0
  14. package/dist/algo/orchestrator.d.ts +8 -1
  15. package/dist/algo/orchestrator.js +15 -3
  16. package/dist/algo/publishEligibility.d.ts +34 -0
  17. package/dist/algo/publishEligibility.js +52 -0
  18. package/dist/algo/ucb1.d.ts +53 -0
  19. package/dist/algo/ucb1.js +156 -0
  20. package/dist/assetstore/assetSidecarRecords.d.ts +7 -2
  21. package/dist/assetstore/assetSidecarRecords.js +238 -0
  22. package/dist/assetstore/assetSidecarRecovery.js +2 -2
  23. package/dist/assetstore/assetStoreHealth.d.ts +6 -0
  24. package/dist/assetstore/assetStoreHealth.js +56 -14
  25. package/dist/assetstore/assetStoreStorage.d.ts +1 -1
  26. package/dist/assetstore/assetStoreStorage.js +27 -9
  27. package/dist/assetstore/assetSyncLedger.d.ts +86 -0
  28. package/dist/assetstore/assetSyncLedger.js +683 -4
  29. package/dist/assetstore/index.d.ts +1 -0
  30. package/dist/assetstore/index.js +1 -0
  31. package/dist/assetstore/localAssetStoreSnapshot.d.ts +51 -0
  32. package/dist/assetstore/localAssetStoreSnapshot.js +329 -0
  33. package/dist/assetstore/localJsonl.d.ts +4 -2
  34. package/dist/assetstore/localJsonl.js +89 -9
  35. package/dist/assetstore/provenance.d.ts +80 -4
  36. package/dist/assetstore/provenance.js +313 -5
  37. package/dist/assetstore/provider.d.ts +58 -1
  38. package/dist/assetstore/provider.js +97 -6
  39. package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
  40. package/dist/benchmark/antiGeneBenchmark.js +4 -3
  41. package/dist/benchmark/antiGeneRollout.d.ts +2 -0
  42. package/dist/benchmark/antiGeneRollout.js +4 -3
  43. package/dist/benchmark/index.d.ts +2 -1
  44. package/dist/benchmark/index.js +2 -1
  45. package/dist/benchmark/selectionFlatAbstention.d.ts +152 -0
  46. package/dist/benchmark/selectionFlatAbstention.js +481 -0
  47. package/dist/bootstrap/envFingerprint.d.ts +9 -0
  48. package/dist/bootstrap/envFingerprint.js +5 -0
  49. package/dist/bootstrap/index.d.ts +2 -1
  50. package/dist/bootstrap/index.js +2 -1
  51. package/dist/bootstrap/v1EnvCompat.d.ts +111 -0
  52. package/dist/bootstrap/v1EnvCompat.js +280 -0
  53. package/dist/events/eventArchive.d.ts +2 -0
  54. package/dist/events/eventArchive.js +13 -3
  55. package/dist/events/eventSchema.d.ts +7 -7
  56. package/dist/events/eventStore.d.ts +2 -0
  57. package/dist/events/eventStore.js +5 -1
  58. package/dist/events/ingest.d.ts +1 -0
  59. package/dist/events/ingest.js +1 -0
  60. package/dist/events/paths.d.ts +3 -1
  61. package/dist/events/paths.js +4 -0
  62. package/dist/events/public.d.ts +2 -2
  63. package/dist/events/public.js +2 -2
  64. package/dist/events/reports.d.ts +2 -0
  65. package/dist/events/reports.js +4 -0
  66. package/dist/exec/autoExec.d.ts +43 -2
  67. package/dist/exec/autoExec.js +76 -9
  68. package/dist/exec/autonomousCycle.d.ts +22 -4
  69. package/dist/exec/autonomousCycle.js +64 -13
  70. package/dist/exec/claudeBridge.d.ts +32 -7
  71. package/dist/exec/claudeBridge.js +281 -29
  72. package/dist/exec/prompt.js +5 -1
  73. package/dist/exec/runnerRegistry.d.ts +68 -26
  74. package/dist/exec/runnerRegistry.js +307 -72
  75. package/dist/exec/selfPr.js +1 -7
  76. package/dist/feedback/envelope.d.ts +61 -0
  77. package/dist/feedback/envelope.js +168 -0
  78. package/dist/feedback/index.d.ts +1 -0
  79. package/dist/feedback/index.js +1 -0
  80. package/dist/hooks/hooks.js +1 -0
  81. package/dist/hub/assetCallLog.d.ts +35 -1
  82. package/dist/hub/assetCallLog.js +124 -1
  83. package/dist/hub/bindings.d.ts +8 -1
  84. package/dist/hub/bindings.js +17 -6
  85. package/dist/hub/capability.d.ts +90 -3
  86. package/dist/hub/fake.d.ts +2 -2
  87. package/dist/hub/fake.js +1 -1
  88. package/dist/hub/questionGenerator.d.ts +5 -1
  89. package/dist/hub/questionGenerator.js +8 -6
  90. package/dist/index.d.ts +3 -1
  91. package/dist/index.js +4 -1
  92. package/dist/mailbox/dispatch.d.ts +1 -1
  93. package/dist/mailbox/dispatch.js +22 -6
  94. package/dist/mailbox/envelope.d.ts +7 -1
  95. package/dist/mailbox/envelope.js +9 -2
  96. package/dist/mailbox/ipcServer.d.ts +10 -2
  97. package/dist/mailbox/ipcServer.js +163 -13
  98. package/dist/mailbox/store.d.ts +89 -3
  99. package/dist/mailbox/store.js +895 -41
  100. package/dist/ops/evolutionGraphProjection.d.ts +20 -0
  101. package/dist/ops/evolutionGraphProjection.js +315 -0
  102. package/dist/ops/index.d.ts +2 -1
  103. package/dist/ops/index.js +2 -1
  104. package/dist/ops/selfUpdate.d.ts +8 -0
  105. package/dist/ops/selfUpdate.js +24 -8
  106. package/dist/personality/schema.d.ts +16 -16
  107. package/dist/schema/evolutionGraph.d.ts +784 -0
  108. package/dist/schema/evolutionGraph.js +187 -0
  109. package/dist/schema/index.d.ts +1 -0
  110. package/dist/schema/index.js +1 -0
  111. package/dist/schema/signal.d.ts +3 -3
  112. package/dist/schema/signal.js +1 -1
  113. package/dist/signals/curriculum.d.ts +55 -0
  114. package/dist/signals/curriculum.js +202 -0
  115. package/dist/signals/cycleHistoryFromEvents.js +17 -8
  116. package/dist/signals/expand.d.ts +15 -1
  117. package/dist/signals/expand.js +169 -1
  118. package/dist/signals/extractor.d.ts +2 -2
  119. package/dist/signals/extractor.js +31 -6
  120. package/dist/signals/index.d.ts +3 -1
  121. package/dist/signals/index.js +3 -1
  122. package/dist/signals/metaSignals.d.ts +4 -0
  123. package/dist/signals/metaSignals.js +42 -0
  124. package/dist/signals/signalGate.js +1 -1
  125. package/dist/signals/taskDomain.d.ts +22 -0
  126. package/dist/signals/taskDomain.js +43 -0
  127. package/dist/strategy/constraintAblation.js +115 -369
  128. package/dist/strategy/constraintAblationPredicates.d.ts +31 -0
  129. package/dist/strategy/constraintAblationPredicates.js +339 -0
  130. package/dist/trace/index.d.ts +3 -1
  131. package/dist/trace/index.js +3 -1
  132. package/dist/trace/learningTrace.d.ts +209 -0
  133. package/dist/trace/learningTrace.js +276 -0
  134. package/dist/trace/proxyTurns.d.ts +31 -0
  135. package/dist/trace/proxyTurns.js +137 -0
  136. package/dist/verify/sandboxRunner.d.ts +28 -0
  137. package/dist/verify/sandboxRunner.js +218 -19
  138. package/dist/verify/sandboxedValidation.d.ts +9 -0
  139. package/dist/verify/sandboxedValidation.js +113 -13
  140. package/dist/verify/validation.d.ts +11 -1
  141. package/dist/verify/validation.js +31 -0
  142. package/package.json +5 -2
@@ -0,0 +1,168 @@
1
+ export const PRIORITY_AXES = Object.freeze([
2
+ 'task_success',
3
+ 'user_preference',
4
+ 'quality',
5
+ 'safety',
6
+ 'cost',
7
+ 'latency',
8
+ 'other',
9
+ ]);
10
+ export const LABELS = Object.freeze(['positive', 'negative', 'mixed', 'neutral']);
11
+ export const ATTENTION_LEVELS = Object.freeze(['full', 'limited', 'skimmed', 'unknown']);
12
+ export const EVIDENCE_KINDS = Object.freeze([
13
+ 'evolution_event',
14
+ 'evolution_outcome',
15
+ 'user_override',
16
+ 'review',
17
+ 'turn',
18
+ 'external',
19
+ ]);
20
+ export function clamp01(value) {
21
+ const n = Number(value);
22
+ if (!Number.isFinite(n))
23
+ return 0.5;
24
+ if (n < 0)
25
+ return 0;
26
+ if (n > 1)
27
+ return 1;
28
+ return n;
29
+ }
30
+ export function labelFromScalar(value) {
31
+ const scalar = clamp01(value);
32
+ if (scalar >= 0.6)
33
+ return 'positive';
34
+ if (scalar <= 0.4)
35
+ return 'negative';
36
+ return 'mixed';
37
+ }
38
+ export function normalizeAttention(input) {
39
+ const record = asRecord(input);
40
+ if (!record)
41
+ return { level: 'unknown' };
42
+ const attention = {
43
+ level: enumValue(record['level'], ATTENTION_LEVELS, 'unknown'),
44
+ };
45
+ const observedItems = nonNegativeInteger(record['observed_items']);
46
+ const elapsedMs = nonNegativeInteger(record['elapsed_ms']);
47
+ if (observedItems !== null)
48
+ attention.observed_items = observedItems;
49
+ if (elapsedMs !== null)
50
+ attention.elapsed_ms = elapsedMs;
51
+ return attention;
52
+ }
53
+ export function evidenceRef(kind, id, options = {}) {
54
+ const ref = {
55
+ kind: enumValue(kind, EVIDENCE_KINDS, 'external'),
56
+ id: String(id ?? '').trim() || 'unknown',
57
+ };
58
+ if (typeof options.summary === 'string' && options.summary.trim()) {
59
+ ref.summary = options.summary.trim();
60
+ }
61
+ return ref;
62
+ }
63
+ export function normalizeEvidenceRef(input) {
64
+ const record = asRecord(input);
65
+ if (!record)
66
+ return evidenceRef('external', 'unknown');
67
+ return evidenceRef(record['kind'], record['id'], { summary: record['summary'] });
68
+ }
69
+ export function envelopeUncertainty(scalar, attentionLevel, indecision, conflict) {
70
+ const normalizedScalar = clamp01(scalar);
71
+ const ambiguity = 1 - Math.abs(normalizedScalar - 0.5) * 2;
72
+ const attentionPenalty = {
73
+ full: 0,
74
+ limited: 0.15,
75
+ skimmed: 0.30,
76
+ unknown: 0.20,
77
+ };
78
+ return clamp01(0.10
79
+ + 0.30 * ambiguity
80
+ + attentionPenalty[attentionLevel]
81
+ + (indecision ? 0.25 : 0)
82
+ + (conflict ? 0.35 : 0));
83
+ }
84
+ export function fromScalarFeedback(options = {}) {
85
+ const input = options ?? {};
86
+ const scalar = clamp01(input.scalar);
87
+ const label = labelFromScalar(scalar);
88
+ const indecision = Boolean(input.indecision) || label === 'mixed';
89
+ const conflict = Boolean(input.conflict);
90
+ const evaluatorAttention = normalizeAttention(input.evaluator_attention ?? input.evaluatorAttention);
91
+ const priorityAxis = enumValue(input.priority_axis ?? input.priorityAxis, PRIORITY_AXES, 'task_success');
92
+ const evidence = input.evidence_ref ?? input.evidenceRef ?? evidenceRef('external', 'unknown');
93
+ return {
94
+ priority_axis: priorityAxis,
95
+ label,
96
+ scalar,
97
+ indecision,
98
+ conflict,
99
+ evaluator_attention: evaluatorAttention,
100
+ evidence_ref: normalizeEvidenceRef(evidence),
101
+ uncertainty: envelopeUncertainty(scalar, evaluatorAttention.level, indecision, conflict),
102
+ };
103
+ }
104
+ export function fromOutcomeScalar(outcome, options = {}) {
105
+ const record = asRecord(outcome);
106
+ if (!record)
107
+ return null;
108
+ const scalar = record['user_override'] ?? record['score'];
109
+ if (scalar === null || scalar === undefined)
110
+ return null;
111
+ return fromScalarFeedback({ ...options, scalar });
112
+ }
113
+ export function withConflict(envelope) {
114
+ const evaluatorAttention = normalizeAttention(envelope.evaluator_attention);
115
+ return {
116
+ ...envelope,
117
+ conflict: true,
118
+ evaluator_attention: evaluatorAttention,
119
+ uncertainty: envelopeUncertainty(envelope.scalar, evaluatorAttention.level, envelope.indecision, true),
120
+ };
121
+ }
122
+ export function withIndecision(envelope) {
123
+ const evaluatorAttention = normalizeAttention(envelope.evaluator_attention);
124
+ return {
125
+ ...envelope,
126
+ indecision: true,
127
+ evaluator_attention: evaluatorAttention,
128
+ uncertainty: envelopeUncertainty(envelope.scalar, evaluatorAttention.level, true, envelope.conflict),
129
+ };
130
+ }
131
+ export function aggregateFeedbackEnvelopes(envelopes) {
132
+ const list = Array.isArray(envelopes) ? envelopes.filter(Boolean) : [];
133
+ if (list.length === 0)
134
+ return { dominant_label: null, uncertainty: 1, sample_count: 0 };
135
+ const sampleCount = list.length;
136
+ const meanUncertainty = list.reduce((sum, envelope) => sum + clamp01(envelope.uncertainty), 0) / sampleCount;
137
+ const hasPositive = list.some((envelope) => envelope.label === 'positive');
138
+ const hasNegative = list.some((envelope) => envelope.label === 'negative');
139
+ const hasConflict = list.some((envelope) => envelope.conflict) || (hasPositive && hasNegative);
140
+ const hasIndecision = list.some((envelope) => envelope.indecision || envelope.label === 'mixed' || envelope.label === 'neutral');
141
+ const hasLowAttention = list.some((envelope) => normalizeAttention(envelope.evaluator_attention).level !== 'full');
142
+ const uncertainty = clamp01(meanUncertainty
143
+ + (hasConflict ? 0.25 : 0)
144
+ + (hasIndecision ? 0.10 : 0)
145
+ + (hasLowAttention ? 0.10 : 0));
146
+ let dominantLabel = null;
147
+ if (!hasConflict && !hasLowAttention && uncertainty < 0.5) {
148
+ if (hasPositive)
149
+ dominantLabel = 'positive';
150
+ else if (hasNegative)
151
+ dominantLabel = 'negative';
152
+ }
153
+ return { dominant_label: dominantLabel, uncertainty, sample_count: sampleCount };
154
+ }
155
+ function enumValue(value, allowed, fallback) {
156
+ return typeof value === 'string' && allowed.includes(value)
157
+ ? value
158
+ : fallback;
159
+ }
160
+ function nonNegativeInteger(value) {
161
+ const n = Number(value);
162
+ return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : null;
163
+ }
164
+ function asRecord(value) {
165
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
166
+ ? value
167
+ : null;
168
+ }
@@ -0,0 +1 @@
1
+ export * from './envelope.js';
@@ -0,0 +1 @@
1
+ export * from './envelope.js';
@@ -69,6 +69,7 @@ export function composeSessionStartWithRecap(base, recapCtx, estimateTokensOrOpt
69
69
  export function signalsFromToolUse(event) {
70
70
  const turn = {
71
71
  toolName: event.toolName,
72
+ ...(event.toolResult ? { toolResult: event.toolResult } : {}),
72
73
  ...(event.isError && event.toolResult ? { errorMessage: event.toolResult } : {}),
73
74
  ...(event.text ? { text: event.text } : {}),
74
75
  isMeta: false,
@@ -1,4 +1,5 @@
1
- export type AssetCallAction = 'hub_search_hit' | 'hub_search_miss' | 'asset_reuse' | 'asset_reference' | 'asset_publish' | 'asset_publish_skip' | 'hub_review_submitted' | 'hub_review_rejected' | 'hub_review_failed';
1
+ export type AssetCallAction = 'hub_search_hit' | 'hub_search_miss' | 'asset_reuse' | 'asset_reference' | 'asset_publish' | 'asset_publish_skip' | 'asset_inject' | 'asset_inject_shadow' | 'hub_review_submitted' | 'hub_review_rejected' | 'hub_review_failed';
2
+ export type TokensSavedBasis = 'measured' | 'cost_index' | 'estimated_blast_radius' | 'estimated_default';
2
3
  export interface AssetCallEntry {
3
4
  run_id?: string | null;
4
5
  action: AssetCallAction;
@@ -10,6 +11,9 @@ export interface AssetCallEntry {
10
11
  mode?: 'direct' | 'reference';
11
12
  signals?: readonly string[];
12
13
  reason?: string;
14
+ tokens_saved?: number | null;
15
+ tokens_saved_basis?: TokensSavedBasis | null;
16
+ tokens_spent?: number | null;
13
17
  extra?: Record<string, unknown>;
14
18
  }
15
19
  export interface AssetCallRecord extends AssetCallEntry {
@@ -30,6 +34,32 @@ export interface CallLogSummary {
30
34
  by_action: Record<string, number>;
31
35
  entries: AssetCallRecord[];
32
36
  }
37
+ export interface AssetReuseAttribution {
38
+ asset_id: string;
39
+ source_node_id: string | null;
40
+ chain_id: string | null;
41
+ reuse: number;
42
+ reference: number;
43
+ tokens_saved: number;
44
+ }
45
+ export interface ReuseAttributionSummary {
46
+ total_reuse: number;
47
+ total_reference: number;
48
+ total_tokens_saved: number;
49
+ by_asset: AssetReuseAttribution[];
50
+ }
51
+ export interface ReuseSavingsMetric {
52
+ tokens_saved: number;
53
+ tokens_saved_basis: TokensSavedBasis;
54
+ }
55
+ /** Measured derivation cost carried by an asset (or by a Hub wrapper's payload). */
56
+ export declare function assetDerivationTokenCost(asset: unknown): number | undefined;
57
+ /**
58
+ * Attribute savings without inventing a measured value: asset telemetry wins, then this node's publish-cost
59
+ * index, then the savings-core blast/default estimator. Reference reuse applies the same fractional discount
60
+ * to measured and estimated costs.
61
+ */
62
+ export declare function reuseSavingsForAsset(asset: unknown, mode: 'direct' | 'reference', indexedTokenCost?: unknown): ReuseSavingsMetric;
33
63
  export declare class AssetCallLog {
34
64
  private readonly path;
35
65
  private readonly now;
@@ -40,4 +70,8 @@ export declare class AssetCallLog {
40
70
  read(opts?: ReadOpts): AssetCallRecord[];
41
71
  /** Totals + per-action counts (for CLI / observability). */
42
72
  summarize(opts?: ReadOpts): CallLogSummary;
73
+ /** Local-only attribution rollup over reuse/reference audit rows. */
74
+ reuseAttributionSummary(opts?: ReadOpts): ReuseAttributionSummary;
75
+ /** Later valid publish rows win; malformed/non-positive costs never erase a prior measurement. */
76
+ assetCostIndex(opts?: ReadOpts): Record<string, number>;
43
77
  }
@@ -5,6 +5,69 @@
5
5
  // or fails an evolution. The log path is injected so it's testable without a real home dir.
6
6
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from 'node:fs';
7
7
  import { dirname } from 'node:path';
8
+ import { REUSE_ESTIMATOR, reuseEstimate } from '../ops/savingsCore.js';
9
+ function objectRecord(value) {
10
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
11
+ ? value
12
+ : undefined;
13
+ }
14
+ function nonEmptyString(value) {
15
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
16
+ }
17
+ function positiveNumber(value, coerceLegacy = false) {
18
+ const number = typeof value === 'number' ? value : coerceLegacy ? Number(value) : Number.NaN;
19
+ return Number.isFinite(number) && number > 0 ? number : undefined;
20
+ }
21
+ function nestedAssetRecord(asset) {
22
+ const direct = objectRecord(asset);
23
+ return { ...(direct ? { direct } : {}), ...(objectRecord(direct?.['payload']) ? { payload: objectRecord(direct?.['payload']) } : {}) };
24
+ }
25
+ /** Measured derivation cost carried by an asset (or by a Hub wrapper's payload). */
26
+ export function assetDerivationTokenCost(asset) {
27
+ const records = nestedAssetRecord(asset);
28
+ for (const record of [records.direct, records.payload]) {
29
+ const derivation = objectRecord(record?.['derivation_tokens']);
30
+ const total = positiveNumber(derivation?.['total_tokens']);
31
+ if (total !== undefined)
32
+ return total;
33
+ }
34
+ return undefined;
35
+ }
36
+ function assetBlastRadiusLines(asset) {
37
+ const records = nestedAssetRecord(asset);
38
+ for (const record of [records.direct, records.payload]) {
39
+ const blast = objectRecord(record?.['blast_radius']);
40
+ const lines = positiveNumber(blast?.['lines']);
41
+ if (lines !== undefined)
42
+ return lines;
43
+ }
44
+ return undefined;
45
+ }
46
+ /**
47
+ * Attribute savings without inventing a measured value: asset telemetry wins, then this node's publish-cost
48
+ * index, then the savings-core blast/default estimator. Reference reuse applies the same fractional discount
49
+ * to measured and estimated costs.
50
+ */
51
+ export function reuseSavingsForAsset(asset, mode, indexedTokenCost) {
52
+ const measured = assetDerivationTokenCost(asset);
53
+ const indexed = measured === undefined ? positiveNumber(indexedTokenCost) : undefined;
54
+ const cost = measured ?? indexed;
55
+ if (cost !== undefined) {
56
+ return {
57
+ tokens_saved: Math.round(mode === 'reference' ? cost * REUSE_ESTIMATOR.reference_saving_fraction : cost),
58
+ tokens_saved_basis: measured !== undefined ? 'measured' : 'cost_index',
59
+ };
60
+ }
61
+ const estimated = reuseEstimate(assetBlastRadiusLines(asset), mode);
62
+ return { tokens_saved: estimated.tokens_saved, tokens_saved_basis: estimated.basis };
63
+ }
64
+ function isAssetCallRecord(value) {
65
+ const record = objectRecord(value);
66
+ return record !== undefined
67
+ && typeof record['timestamp'] === 'string'
68
+ && typeof record['action'] === 'string'
69
+ && record['action'].length > 0;
70
+ }
8
71
  export class AssetCallLog {
9
72
  path;
10
73
  now;
@@ -33,7 +96,9 @@ export class AssetCallLog {
33
96
  if (!line)
34
97
  continue;
35
98
  try {
36
- entries.push(JSON.parse(line));
99
+ const parsed = JSON.parse(line);
100
+ if (isAssetCallRecord(parsed))
101
+ entries.push(parsed);
37
102
  }
38
103
  catch { /* skip corrupt */ }
39
104
  }
@@ -69,4 +134,62 @@ export class AssetCallLog {
69
134
  }
70
135
  return { total_entries: entries.length, unique_assets: assets.size, unique_runs: runs.size, by_action: byAction, entries };
71
136
  }
137
+ /** Local-only attribution rollup over reuse/reference audit rows. */
138
+ reuseAttributionSummary(opts = {}) {
139
+ const entries = this.read(opts).filter((entry) => entry.action === 'asset_reuse' || entry.action === 'asset_reference');
140
+ const byAsset = new Map();
141
+ let totalTokensSaved = 0;
142
+ let totalReuse = 0;
143
+ let totalReference = 0;
144
+ for (const entry of entries) {
145
+ const id = nonEmptyString(entry.asset_id) ?? '(unknown)';
146
+ let aggregate = byAsset.get(id);
147
+ if (!aggregate) {
148
+ aggregate = {
149
+ asset_id: id,
150
+ source_node_id: nonEmptyString(entry.source_node_id) ?? null,
151
+ chain_id: nonEmptyString(entry.chain_id) ?? null,
152
+ reuse: 0,
153
+ reference: 0,
154
+ tokens_saved: 0,
155
+ };
156
+ byAsset.set(id, aggregate);
157
+ }
158
+ if (entry.action === 'asset_reuse') {
159
+ aggregate.reuse += 1;
160
+ totalReuse += 1;
161
+ }
162
+ else {
163
+ aggregate.reference += 1;
164
+ totalReference += 1;
165
+ }
166
+ const tokensSaved = positiveNumber(entry['tokens_saved'], true);
167
+ if (tokensSaved !== undefined) {
168
+ aggregate.tokens_saved += tokensSaved;
169
+ totalTokensSaved += tokensSaved;
170
+ }
171
+ aggregate.source_node_id ??= nonEmptyString(entry.source_node_id) ?? null;
172
+ aggregate.chain_id ??= nonEmptyString(entry.chain_id) ?? null;
173
+ }
174
+ const byAssetRows = [...byAsset.values()].sort((left, right) => (right.reuse + right.reference) - (left.reuse + left.reference));
175
+ return {
176
+ total_reuse: totalReuse,
177
+ total_reference: totalReference,
178
+ total_tokens_saved: totalTokensSaved,
179
+ by_asset: byAssetRows,
180
+ };
181
+ }
182
+ /** Later valid publish rows win; malformed/non-positive costs never erase a prior measurement. */
183
+ assetCostIndex(opts = {}) {
184
+ const costs = new Map();
185
+ for (const entry of this.read(opts)) {
186
+ if (entry.action !== 'asset_publish')
187
+ continue;
188
+ const assetId = nonEmptyString(entry.asset_id);
189
+ const tokensSpent = positiveNumber(entry['tokens_spent'], true);
190
+ if (assetId && tokensSpent !== undefined)
191
+ costs.set(assetId, tokensSpent);
192
+ }
193
+ return Object.fromEntries(costs);
194
+ }
72
195
  }
@@ -1,4 +1,4 @@
1
- import type { HubCapability, HubBindings } from './capability.js';
1
+ import type { HubCapability, HubBindings, PublishReceipt } from './capability.js';
2
2
  import { type LeakCheckMode } from './sanitize.js';
3
3
  /** publish 回执非 accepted → 抛此错; SyncEngine 据 terminal 决定是否重试. */
4
4
  export declare class PublishRejectedError extends Error {
@@ -19,6 +19,13 @@ export interface HubBindingsOptions {
19
19
  mode?: LeakCheckMode;
20
20
  };
21
21
  }
22
+ /**
23
+ * Internal publish-handler result. `submittedAssetIds` identifies the exact sanitized
24
+ * content sent to the Hub, even when the Hub's accepted receipt omits asset ids.
25
+ */
26
+ export interface PublishDispatchResult extends PublishReceipt {
27
+ submittedAssetIds?: string[];
28
+ }
22
29
  /**
23
30
  * 把 HubCapability 接到 core 两 seam(M6-1):
24
31
  * - asProxyHandler: Dispatcher.handlers.proxy — 按 envelope.type 路由到 hub. 抛错→store.fail()重试;
@@ -37,14 +37,18 @@ export function makeHubBindings(cap, options = {}) {
37
37
  const sanEnabled = sanCfg.enabled ?? true;
38
38
  const sanEnv = sanCfg.env ?? (typeof process !== 'undefined' ? process.env : {});
39
39
  /** Single publish chokepoint: sanitize (on by default) → strict + leak found → refuse (terminal, not retryable) → cap.publish the redacted bundle. */
40
- const publishSanitized = (bundle) => {
40
+ const publishSanitized = async (bundle, publishOptions) => {
41
41
  if (!sanEnabled)
42
- return cap.publish(bundle);
42
+ return publishOptions ? cap.publish(bundle, publishOptions) : cap.publish(bundle);
43
43
  const r = sanitizeBundle(bundle, { env: sanEnv, ...(sanCfg.mode ? { mode: sanCfg.mode } : {}) });
44
44
  if (r.blocked) {
45
45
  throw new PublishRejectedError('leak_blocked', true, `sensitive data detected before publish, refused (not retryable): ${summarizeLeaks(r.leaks)}`);
46
46
  }
47
- return cap.publish(r.bundle);
47
+ const receipt = publishOptions
48
+ ? await cap.publish(r.bundle, publishOptions)
49
+ : await cap.publish(r.bundle);
50
+ const submittedAssetIds = contentAssetIds(r.bundle);
51
+ return submittedAssetIds ? { ...receipt, submittedAssetIds } : receipt;
48
52
  };
49
53
  return {
50
54
  asProxyHandler() {
@@ -54,9 +58,10 @@ export function makeHubBindings(cap, options = {}) {
54
58
  // payload 可为 bundle {assets:[...]} 或单资产; 统一成 bundle 数组发.
55
59
  const p = e.payload;
56
60
  const bundle = Array.isArray(p.assets) ? p.assets : [p];
57
- const r = await publishSanitized(bundle);
58
- if (r.status !== 'accepted')
59
- throw new PublishRejectedError(r.status, r.terminal ?? true, r.reason);
61
+ const r = await publishSanitized(bundle, { idempotencyKey: e.idempotencyKey });
62
+ if (r.status !== 'accepted') {
63
+ throw new PublishRejectedError(r.rejection?.code ?? r.status, r.terminal ?? true, r.reason, r.rejection?.retryAfterMs);
64
+ }
60
65
  return r;
61
66
  }
62
67
  case 'task_claim': {
@@ -98,6 +103,12 @@ export function makeHubBindings(cap, options = {}) {
98
103
  },
99
104
  };
100
105
  }
106
+ function contentAssetIds(bundle) {
107
+ const ids = bundle.map((asset) => asset.asset_id);
108
+ return ids.every((id) => typeof id === 'string' && /^sha256:[a-f0-9]{64}$/i.test(id))
109
+ ? ids
110
+ : undefined;
111
+ }
101
112
  function firstString(...values) {
102
113
  for (const value of values) {
103
114
  if (typeof value !== 'string')
@@ -16,6 +16,11 @@ export interface PublishReceipt {
16
16
  assetIds?: string[];
17
17
  /** 终态标记: true=不可重试(reject/quarantine/402), SyncEngine 不得 incrementRetry(money-safety risk). */
18
18
  terminal?: boolean;
19
+ /** Adapter-owned structured rejection metadata; callers must not infer policy from free-form `reason`. */
20
+ rejection?: {
21
+ code: 'invalid_request' | 'credit_shortage' | 'node_unauthorized' | 'not_found' | 'duplicate' | 'invalid_payload' | 'cooldown' | 'hub_rejected';
22
+ retryAfterMs?: number;
23
+ };
19
24
  /** M8-1: 公版经济侧只读回执(core 不解释经济语义, 仅透传供观测/UI). private hub 多为空. */
20
25
  economic?: {
21
26
  creditShortage?: boolean;
@@ -25,6 +30,10 @@ export interface PublishReceipt {
25
30
  gdiScore?: number;
26
31
  };
27
32
  }
33
+ export interface PublishOptions {
34
+ /** Stable, non-blank per logical publish so adapters can make transport retries idempotent. */
35
+ idempotencyKey?: string;
36
+ }
28
37
  /** 资产查询. 复用 assetstore SearchQuery, 不发明第二套查询 DSL. */
29
38
  export type HubQuery = SearchQuery;
30
39
  /** task 子系统事件(订阅流元素). 公版含经济上下文(bounty/credit), 私有仅 workflow. */
@@ -111,6 +120,75 @@ export interface ReuseResultReceipt {
111
120
  reason?: string;
112
121
  id?: string;
113
122
  }
123
+ export type LearningAssetType = 'memory' | 'skill' | 'playbook' | 'eval';
124
+ export type LearningAssetStatus = 'pending_review' | 'active' | 'disabled' | 'stale' | 'archived';
125
+ export type LearningAssetEffectiveStatus = LearningAssetStatus | 'expired';
126
+ export type LearningAssetUsageOutcome = ReuseResultOutcome;
127
+ export interface LearningAssetOwner {
128
+ node_id?: string | null;
129
+ user_id?: string | null;
130
+ org_id?: string | null;
131
+ workspace_id?: string | null;
132
+ }
133
+ /** Hub-owned learning asset record. Core keeps it opaque; adapters own wire shape and auth. */
134
+ export interface LearningAssetRecord {
135
+ asset_id: string;
136
+ type: LearningAssetType;
137
+ source_packet_id?: string;
138
+ scope?: string[];
139
+ owner?: LearningAssetOwner;
140
+ title?: string;
141
+ summary?: string;
142
+ confidence?: number;
143
+ last_used_at?: string | null;
144
+ usage_count?: number;
145
+ success_count?: number;
146
+ success_rate?: number;
147
+ expires_at?: string | null;
148
+ conflicts_with?: string[];
149
+ status?: LearningAssetStatus;
150
+ effective_status?: LearningAssetEffectiveStatus;
151
+ status_reason?: string;
152
+ provenance?: Record<string, unknown>;
153
+ payload?: Record<string, unknown>;
154
+ created_at?: string | null;
155
+ updated_at?: string | null;
156
+ [k: string]: unknown;
157
+ }
158
+ export interface LearningAssetListOptions {
159
+ type?: LearningAssetType;
160
+ scope?: readonly string[];
161
+ status?: LearningAssetStatus | readonly LearningAssetStatus[];
162
+ includeExpired?: boolean;
163
+ includePayload?: boolean;
164
+ limit?: number;
165
+ }
166
+ export interface LearningAssetListResult {
167
+ assets: LearningAssetRecord[];
168
+ limit: number;
169
+ reason?: string;
170
+ }
171
+ export interface LearningAssetUsageReport {
172
+ assetId?: string;
173
+ usedAssetIds?: readonly string[];
174
+ outcome: LearningAssetUsageOutcome;
175
+ score?: number;
176
+ sourceEventId: string;
177
+ reason?: string;
178
+ }
179
+ export interface LearningAssetUsageResultRow {
180
+ asset_id: string;
181
+ recorded: boolean;
182
+ duplicated?: boolean;
183
+ error?: string;
184
+ [k: string]: unknown;
185
+ }
186
+ /** Learning-asset usage reporting is best-effort; failures must not break runtime solve paths. */
187
+ export interface LearningAssetUsageReceipt {
188
+ recorded: boolean;
189
+ reason?: string;
190
+ results: LearningAssetUsageResultRow[];
191
+ }
114
192
  /**
115
193
  * Pre-publish dry-run result. The hub runs the same hub-side quality + content-safety
116
194
  * gate as publish but stores nothing and charges no credits, so an agent can check a
@@ -141,6 +219,10 @@ export interface RecipeCreateRequest {
141
219
  pricePerExecution?: number;
142
220
  currency?: string;
143
221
  maxConcurrent?: number;
222
+ idempotencyKey?: string;
223
+ }
224
+ export interface RecipePublishOptions {
225
+ idempotencyKey?: string;
144
226
  }
145
227
  export interface RecipeReceipt {
146
228
  recipeId?: string;
@@ -159,11 +241,11 @@ export interface RecipeExpressionReceipt extends RecipeReceipt {
159
241
  /** Optional Hub recipe/DNA capability. Core stays wire-agnostic; adapters own REST shape. */
160
242
  export interface RecipeCapability {
161
243
  create(request: RecipeCreateRequest): Promise<RecipeReceipt>;
162
- publish(recipeId: string): Promise<RecipeReceipt>;
244
+ publish(recipeId: string, options?: RecipePublishOptions): Promise<RecipeReceipt>;
163
245
  get(recipeId: string): Promise<RecipeFetchReceipt>;
164
246
  express(recipeId: string, request?: RecipeExpressRequest): Promise<RecipeExpressionReceipt>;
165
247
  }
166
- export type HubCapabilityName = 'publish' | 'fetch' | 'search' | 'task' | 'mailbox' | 'auth' | 'audit' | 'air_gap' | 'tenant_isolation' | 'marketplace' | 'economy' | 'questions' | 'recipes' | 'agent_directory';
248
+ export type HubCapabilityName = 'publish' | 'fetch' | 'search' | 'task' | 'mailbox' | 'auth' | 'audit' | 'air_gap' | 'tenant_isolation' | 'marketplace' | 'economy' | 'questions' | 'recipes' | 'agent_directory' | 'learning_assets';
167
249
  /** hub 能力清单(可选). core 据此动态适配 UI/行为, "按需耦合". */
168
250
  export interface HubManifest {
169
251
  capabilities: HubCapabilityName[] | string[];
@@ -201,6 +283,7 @@ export interface HttpRequestLike {
201
283
  method: string;
202
284
  path: string;
203
285
  body?: string;
286
+ signal?: AbortSignal;
204
287
  }
205
288
  /**
206
289
  * Pluggable auth. Example implementations: public OAuth device token / Ed25519 keypair / enterprise SSO (OIDC/LDAP).
@@ -222,7 +305,7 @@ export interface HubCapability {
222
305
  * 异步发布. 公版 /a2a/publish 收 **bundle**[Gene,Capsule,(EvolutionEvent)](一个 cycle 的产物一起发, +GDI);
223
306
  * 传单资产=[asset]。core 已 normalizeForPut(算/校验 asset_id), hub 做经济/治理/质量 gate。
224
307
  */
225
- publish(bundle: AssetRecord[]): Promise<PublishReceipt>;
308
+ publish(bundle: AssetRecord[], options?: PublishOptions): Promise<PublishReceipt>;
226
309
  /** 拉取资产. hub 侧排序/scope(公版 GDI, 私有 RBAC); core 不重排. */
227
310
  fetch(query: HubQuery): Promise<AssetRecord[]>;
228
311
  /** 搜索资产. 与 fetch 同 shape, 差异在 hub 侧召回策略. */
@@ -231,6 +314,10 @@ export interface HubCapability {
231
314
  fetchAssetById?(assetId: string): Promise<AssetRecord | null>;
232
315
  /** 可选: 报告某个资产被复用后的结果. PHub/enterprise adapter 实现; 不支持时 runtime 应明确降级. */
233
316
  recordReuseResult?(report: ReuseResultReport): Promise<ReuseResultReceipt>;
317
+ /** 可选: 列出 hub 端 runtime learning assets. Missing means the adapter does not support this MVP surface. */
318
+ listLearningAssets?(options?: LearningAssetListOptions): Promise<LearningAssetListResult>;
319
+ /** 可选: 上报 learning asset 使用结果. Best-effort; failures must never break solve paths. */
320
+ recordLearningAssetUsage?(report: LearningAssetUsageReport): Promise<LearningAssetUsageReceipt>;
234
321
  /**
235
322
  * 可选: 发布前 dry-run 校验(公版 POST /a2a/validate). 跑与 publish 相同的 hub 端质量门禁 + 内容安全扫描,
236
323
  * 但不落库、不计费; 经由 Proxy 暴露时调用方需先执行与 publish 相同的客户端脱敏/泄漏拦截.
@@ -1,4 +1,4 @@
1
- import type { HubCapability, PublishReceipt, AgentEvent, TaskEvent, AuthProvider, HubQuery, AssetRecord, TaskCompleteContext } from './capability.js';
1
+ import type { HubCapability, PublishReceipt, AgentEvent, TaskEvent, AuthProvider, HubQuery, AssetRecord, TaskCompleteContext, PublishOptions } from './capability.js';
2
2
  export interface FakeHubOptions {
3
3
  /** 脚本化 publish gate: 返回 reject/quarantine 以测 PublishReceipt 终态路径. */
4
4
  publishGate?: (asset: AssetRecord) => Pick<PublishReceipt, 'status' | 'reason' | 'terminal'>;
@@ -28,7 +28,7 @@ export declare class FakeHubCapability implements HubCapability {
28
28
  constructor(opts?: FakeHubOptions);
29
29
  /** 测试注入: 排一条 inbound 事件供 poll 拉. */
30
30
  seedInbound(e: AgentEvent): void;
31
- publish(bundle: AssetRecord[]): Promise<PublishReceipt>;
31
+ publish(bundle: AssetRecord[], _options?: PublishOptions): Promise<PublishReceipt>;
32
32
  fetch(query: HubQuery): Promise<AssetRecord[]>;
33
33
  search(query: HubQuery): Promise<AssetRecord[]>;
34
34
  task: {
package/dist/hub/fake.js CHANGED
@@ -26,7 +26,7 @@ export class FakeHubCapability {
26
26
  }
27
27
  /** 测试注入: 排一条 inbound 事件供 poll 拉. */
28
28
  seedInbound(e) { this.inbox.push(e); }
29
- async publish(bundle) {
29
+ async publish(bundle, _options) {
30
30
  const assetIds = bundle.map((a) => computeAssetId(a));
31
31
  const gate = this.opts.publishGate?.(bundle[0] ?? {}) ?? { status: 'accepted' };
32
32
  if (gate.status === 'accepted')
@@ -54,6 +54,10 @@ export interface QuestionGenerationResult {
54
54
  state: QuestionGeneratorState;
55
55
  changed: boolean;
56
56
  }
57
- export declare function extractTopicKeywords(transcript: string | undefined, memory: string | undefined, max?: number): string[];
57
+ export interface TopicKeywordOptions {
58
+ minOccurrences?: number;
59
+ preserveSourceOrder?: boolean;
60
+ }
61
+ export declare function extractTopicKeywords(transcript: string | undefined, memory: string | undefined, max?: number, opts?: TopicKeywordOptions): string[];
58
62
  export declare function generateQuestions(input?: GenerateQuestionsInput): QuestionGenerationResult;
59
63
  export declare function generateUrgentQuestions(input?: GenerateUrgentQuestionsInput): QuestionGenerationResult;