@evomap/evolver-core 2.0.0-beta.1 → 2.0.0-beta.3
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/assets/gep/genes.jsonl +5 -0
- package/dist/algo/candidateAssembly.js +23 -14
- package/dist/algo/capabilityCandidates.d.ts +2 -0
- package/dist/algo/capabilityCandidates.js +5 -0
- package/dist/algo/conversationSniffer.d.ts +18 -0
- package/dist/algo/conversationSniffer.js +132 -0
- package/dist/algo/cycleEngine.d.ts +11 -0
- package/dist/algo/cycleEngine.js +14 -6
- package/dist/algo/cycleFailureClassifier.d.ts +1 -1
- package/dist/algo/cycleFailureClassifier.js +13 -5
- package/dist/algo/geneIntake.d.ts +14 -4
- package/dist/algo/geneIntake.js +37 -9
- package/dist/algo/geneSelection.d.ts +18 -1
- package/dist/algo/geneSelection.js +42 -18
- package/dist/algo/index.d.ts +2 -0
- package/dist/algo/index.js +2 -0
- package/dist/algo/memoryGraph.d.ts +62 -0
- package/dist/algo/memoryGraph.js +86 -0
- package/dist/algo/orchestrator.d.ts +3 -0
- package/dist/algo/orchestrator.js +14 -2
- package/dist/assetstore/assetSidecarRecords.d.ts +23 -0
- package/dist/assetstore/assetSidecarRecords.js +142 -0
- package/dist/assetstore/assetSidecarRecovery.d.ts +48 -0
- package/dist/assetstore/assetSidecarRecovery.js +288 -0
- package/dist/assetstore/assetStoreHealth.d.ts +75 -0
- package/dist/assetstore/assetStoreHealth.js +277 -0
- package/dist/assetstore/assetStoreLayout.d.ts +2 -0
- package/dist/assetstore/assetStoreLayout.js +6 -0
- package/dist/assetstore/assetStoreStorage.d.ts +42 -0
- package/dist/assetstore/assetStoreStorage.js +318 -0
- package/dist/assetstore/assetSyncLedger.d.ts +32 -0
- package/dist/assetstore/assetSyncLedger.js +66 -0
- package/dist/assetstore/index.d.ts +5 -1
- package/dist/assetstore/index.js +5 -1
- package/dist/assetstore/localJsonl.d.ts +10 -2
- package/dist/assetstore/localJsonl.js +93 -37
- package/dist/assetstore/pendingSignals.js +3 -1
- package/dist/assetstore/provenance.d.ts +20 -2
- package/dist/assetstore/provenance.js +92 -56
- package/dist/assetstore/provider.d.ts +2 -0
- package/dist/assetstore/reviewFilter.js +5 -2
- package/dist/assetstore/reviewLedger.d.ts +14 -2
- package/dist/assetstore/reviewLedger.js +78 -43
- package/dist/assetstore/seedGenes.d.ts +3 -0
- package/dist/assetstore/seedGenes.js +134 -0
- package/dist/benchmark/antiGeneBenchmark.d.ts +2 -0
- package/dist/benchmark/antiGeneBenchmark.js +11 -1
- package/dist/benchmark/antiGeneImpact.d.ts +16 -0
- package/dist/benchmark/antiGeneImpact.js +34 -0
- package/dist/benchmark/antiGeneRollout.d.ts +2 -0
- package/dist/benchmark/antiGeneRollout.js +13 -1
- package/dist/benchmark/index.d.ts +2 -1
- package/dist/benchmark/index.js +2 -1
- package/dist/benchmark/triggerShift.d.ts +62 -0
- package/dist/benchmark/triggerShift.js +106 -0
- package/dist/events/eventArchive.d.ts +65 -0
- package/dist/events/eventArchive.js +343 -0
- package/dist/events/eventStore.js +2 -12
- package/dist/events/ingest.d.ts +1 -1
- package/dist/events/ingest.js +2 -0
- package/dist/events/paths.d.ts +1 -1
- package/dist/events/paths.js +2 -2
- package/dist/events/public.d.ts +3 -1
- package/dist/events/public.js +2 -1
- package/dist/events/retention.d.ts +24 -1
- package/dist/events/retention.js +133 -37
- package/dist/exec/autoExec.d.ts +6 -1
- package/dist/exec/autoExec.js +34 -0
- package/dist/exec/autonomousCycle.d.ts +2 -0
- package/dist/exec/autonomousCycle.js +5 -0
- package/dist/exec/claudeBridge.d.ts +7 -2
- package/dist/exec/claudeBridge.js +115 -14
- package/dist/exec/prompt.js +9 -0
- package/dist/exec/runnerRegistry.d.ts +64 -18
- package/dist/exec/runnerRegistry.js +341 -38
- package/dist/exec/selfPrObfuscation.d.ts +2 -1
- package/dist/exec/selfPrObfuscation.js +19 -6
- package/dist/hub/agentDirectory.d.ts +90 -0
- package/dist/hub/agentDirectory.js +104 -0
- package/dist/hub/bindings.js +23 -3
- package/dist/hub/capability.d.ts +14 -2
- package/dist/hub/fake.d.ts +4 -2
- package/dist/hub/fake.js +3 -2
- package/dist/hub/index.d.ts +1 -0
- package/dist/hub/index.js +1 -0
- package/dist/mailbox/catalog.js +3 -0
- package/dist/mailbox/ipcServer.js +2 -2
- package/dist/mailbox/store.d.ts +14 -0
- package/dist/mailbox/store.js +44 -2
- package/dist/material/consumer.js +9 -6
- package/dist/material/index.d.ts +1 -0
- package/dist/material/index.js +1 -0
- package/dist/material/materialArchive.d.ts +81 -0
- package/dist/material/materialArchive.js +466 -0
- package/dist/material/materialStore.d.ts +1 -0
- package/dist/material/materialStore.js +6 -13
- package/dist/ops/savingsCore.js +1 -2
- package/dist/ops/selfUpdate.d.ts +10 -1
- package/dist/ops/selfUpdate.js +64 -15
- package/dist/schema/common.d.ts +1 -1
- package/dist/schema/common.js +1 -1
- package/dist/schema/material.d.ts +5 -5
- package/dist/trace/trajectoryExport.js +2 -2
- package/dist/util/fileLock.d.ts +19 -2
- package/dist/util/fileLock.js +166 -31
- package/dist/wire/geneHints.d.ts +42 -1
- package/dist/wire/geneHints.js +52 -1
- package/dist/wire/index.d.ts +14 -2
- package/dist/wire/index.js +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
{"type":"Gene","schema_version":"1.12.1","id":"gene_seed_publish_feishu_doc","category":"innovate","signals_match":["publish_markdown_to_feishu","create_feishu_doc","export_report_to_feishu","publish_results_to_lark_doc","feishu_doc","lark_doc"],"preconditions":["lark-cli is installed and available on PATH.","lark-cli auth status reports a ready user or bot identity."],"strategy":["Run lark-cli doctor and require an ok result before publishing.","Use the Docs v2 API and avoid deprecated Docs v1 flags.","Write long Markdown content to a temporary file and pass it as an @file input.","Create or update the document with the intended user or bot identity, then return the document URL.","Never overwrite local lark-cli credential files while preparing the document."],"constraints":{"max_files":2,"forbidden_paths":[".git","node_modules","~/.lark-cli/config.json"]},"validation":["node --version"],"summary":"Publish Markdown content as a Feishu/Lark document through the official CLI, using file-backed content and Docs v2.","asset_id":"sha256:4cdcf2b2a30bcb29a3310360c3cb16c7fc7d427d23a43c836ffa5076cfe3c041"}
|
|
2
|
+
{"type":"Gene","schema_version":"1.12.1","id":"gene_seed_conventional_git_commit","category":"optimize","signals_match":["git_commit","create_commit","commit_changes","conventional_commit","stage_and_commit","write_commit_message"],"preconditions":["A git repository has staged or unstaged changes that should become one logical commit."],"strategy":["Inspect git status plus the staged or unstaged diff before choosing a commit shape.","Pick a Conventional Commits type and optional scope from the actual changed files.","Stage only the intended logical files and never stage credentials or unrelated changes.","Write a present-tense imperative subject under 72 characters, with body or footer when needed.","Do not amend, force-push, hard-reset, or bypass hooks without explicit operator approval."],"constraints":{"max_files":50,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Create a focused Conventional Commits-style git commit from the real diff while avoiding secrets and unrelated changes.","asset_id":"sha256:8f0acd8af3d0a94d6f50e7fe20d96ff39dc982022b851b204eddce47e177f467"}
|
|
3
|
+
{"type":"Gene","schema_version":"1.12.1","id":"gene_seed_poll_bugbot_review","category":"optimize","signals_match":["poll_bugbot","bugbot_review","wait_for_ci_review","pr_review_gate","cursor_bugbot","pr_opened"],"preconditions":["An open GitHub PR exists and Cursor Bugbot is expected to review it."],"strategy":["Poll the Cursor Bugbot check by name until it reaches a terminal conclusion or the operator timeout is reached.","Treat SUCCESS as mergeable only when required checks are green and there are no unresolved Bugbot comments.","Treat NEUTRAL as not passed; fetch and summarize Cursor Bugbot inline comments for the operator.","On FAILURE or ACTION_REQUIRED, surface the findings and do not merge automatically.","Only squash-merge when the operator explicitly asked for merge and the review gate is clean."],"constraints":{"max_files":1,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Wait for Cursor Bugbot on a GitHub PR and gate merge decisions on the actual check conclusion and comments.","asset_id":"sha256:87737bf9d2702667c31e336fc79096f4da7f12a84baae25e827ad62b4a08d332"}
|
|
4
|
+
{"type":"Gene","schema_version":"1.12.1","id":"gene_seed_gateway_timeout_recovery","category":"repair","signals_match":["gateway_timeout","upstream_timeout","http_524","http_504","request_timed_out","retry_on_timeout"],"preconditions":["A tool call, fetch, subagent request, or long command returned a gateway-class timeout."],"strategy":["Treat gateway timeout as transient or size-driven until one bounded recovery attempt proves otherwise.","Retry the exact same operation once, then stop retrying the monolithic call.","If the retry also times out, split the work by file, directory, endpoint, record, section, or time window.","Run independent slices in parallel when safe and merge the results.","Apply the same decomposition recursively only to the slice that still times out."],"constraints":{"max_files":1,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Recover from gateway or upstream timeouts by retrying once, then decomposing the work instead of looping the same large call.","asset_id":"sha256:fe6820477e47a8eb848ecfaa33cdb4242d9af77b78fac21fa1d716244984615c"}
|
|
5
|
+
{"type":"Gene","schema_version":"1.12.1","id":"gene_seed_github_webhook_listener","category":"innovate","signals_match":["github_webhook_listener","bugbot_webhook","passive_pr_notifications","notify_when_bugbot_finishes","webhook_tunnel","github_pr_inbox"],"preconditions":["A developer machine can run a local listener and an outbound tunnel for GitHub webhook delivery."],"strategy":["Use an HMAC-validated local listener for GitHub webhook payloads and deduplicate delivery ids.","Expose the listener through an outbound-only tunnel rather than an inbound public port.","Write vetted notifications into a local inbox with restrictive permissions; never execute webhook payloads.","On session start, re-fetch PR state through GitHub before surfacing any inbox item.","Rotate webhook secrets periodically and immediately after suspected exposure."],"constraints":{"max_files":20,"forbidden_paths":[".git","node_modules"]},"validation":["node --version"],"summary":"Deploy a local HMAC-validated GitHub webhook listener that turns PR and Bugbot events into safe operator notifications.","asset_id":"sha256:a64e8d0a91cebd54290451527987230572d8a5bfe90b6ca73cd3368fd3d1f689"}
|
|
@@ -2,7 +2,7 @@ import { aggregateLearningHistory } from '../assetstore/learningHistory.js';
|
|
|
2
2
|
import { reuseSentiment } from '../ops/reuseOutcomes.js';
|
|
3
3
|
import { tagOverlapScore } from '../signals/expand.js';
|
|
4
4
|
import { bannedGenesFromFailures } from './bans.js';
|
|
5
|
-
import {
|
|
5
|
+
import { geneGenerationSource } from './geneIntake.js';
|
|
6
6
|
function asStrings(v) {
|
|
7
7
|
return Array.isArray(v) ? v.filter((x) => typeof x === 'string') : [];
|
|
8
8
|
}
|
|
@@ -18,14 +18,15 @@ function candidateIds(geneId, assetId) {
|
|
|
18
18
|
function isBannedCandidate(banned, geneId, assetId) {
|
|
19
19
|
return candidateIds(geneId, assetId).some((id) => banned.has(id));
|
|
20
20
|
}
|
|
21
|
-
function passesInjectedCandidateGates(candidate, opts) {
|
|
21
|
+
function passesInjectedCandidateGates(candidate, opts, provenance, review) {
|
|
22
22
|
const assetId = candidate.assetId;
|
|
23
23
|
if (opts.provenance && !opts.includeUntrusted) {
|
|
24
|
-
if (!assetId ||
|
|
24
|
+
if (!assetId || provenance?.get(assetId)?.trusted !== true)
|
|
25
25
|
return false;
|
|
26
26
|
}
|
|
27
27
|
if (opts.review) {
|
|
28
|
-
|
|
28
|
+
const reviewRecord = assetId ? review?.get(assetId) : undefined;
|
|
29
|
+
if (!assetId || (reviewRecord !== undefined && reviewRecord.state !== 'approved'))
|
|
29
30
|
return false;
|
|
30
31
|
}
|
|
31
32
|
return true;
|
|
@@ -79,20 +80,23 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
|
|
|
79
80
|
const genes = await store.list('Gene', limit);
|
|
80
81
|
const banned = await computeBans(store, signals, limit);
|
|
81
82
|
const sigSet = new Set(signals);
|
|
83
|
+
const provenance = opts.provenance?.snapshot();
|
|
84
|
+
const review = opts.review?.snapshot();
|
|
82
85
|
const out = [];
|
|
83
86
|
const distilledFallback = [];
|
|
84
87
|
const antiWarnings = [];
|
|
85
88
|
for (const g of genes) {
|
|
86
89
|
// Trust-first (#30): untrusted (e.g. hub-ingested) genes are excluded from the candidate pool by default;
|
|
87
90
|
// they enter only with includeUntrusted or after an explicit promotion. Provenance is keyed by asset_id.
|
|
88
|
-
if (opts.provenance && !opts.includeUntrusted &&
|
|
91
|
+
if (opts.provenance && !opts.includeUntrusted && provenance?.get(String(g.asset_id))?.trusted === false)
|
|
89
92
|
continue;
|
|
90
93
|
// Review-first (#89/#91): auto-distilled drafts land quarantined until a human approves; rejected drafts stay
|
|
91
94
|
// out too. No record → eligible (cycle/migrate genes). Symmetric to the provenance trust-first filter above.
|
|
92
95
|
// Probation (#306): includeProbation lets a quarantined draft be TRIED so it can earn evidence; a rejected
|
|
93
96
|
// draft is never tried. Off (default) keeps quarantined drafts out until approval.
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
const reviewRecord = review?.get(String(g.asset_id));
|
|
98
|
+
if (opts.review && reviewRecord !== undefined && reviewRecord.state !== 'approved'
|
|
99
|
+
&& (!opts.includeProbation || reviewRecord.state === 'rejected'))
|
|
96
100
|
continue;
|
|
97
101
|
const geneId = typeof g['id'] === 'string' ? String(g['id']) : String(g.asset_id);
|
|
98
102
|
const assetId = String(g.asset_id);
|
|
@@ -104,18 +108,20 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
|
|
|
104
108
|
const literal = signalsMatch.some((m) => sigSet.has(m));
|
|
105
109
|
const relevant = literal || tagOverlapScore(signals, { signalsMatch, geneId, category, summary }) > 0;
|
|
106
110
|
if (!relevant) {
|
|
107
|
-
// #97: a trusted, approved, non-banned distilled gene that doesn't match the live signals is not
|
|
108
|
-
// candidate, but it IS eligible as a last-resort fallback (selection uses it only when nothing clears
|
|
109
|
-
// floor).
|
|
110
|
-
//
|
|
111
|
+
// #97: a trusted, approved, non-banned distilled (or evolved) gene that doesn't match the live signals is not
|
|
112
|
+
// a normal candidate, but it IS eligible as a last-resort fallback (selection uses it only when nothing clears
|
|
113
|
+
// the floor). Eligibility here is gated by trust/review/ban, not by skill provenance. Provenance is read from
|
|
114
|
+
// generation_meta (V1 #302); a legacy gene without it falls back to the `gene_distilled_` id namespace.
|
|
111
115
|
// Lightweight: no learning-history aggregation — it is picked first-match, not ranked by health.
|
|
112
|
-
|
|
116
|
+
const gsrc = geneGenerationSource(g, geneId);
|
|
117
|
+
if (gsrc === 'distilled' || gsrc === 'evolved') {
|
|
113
118
|
distilledFallback.push({
|
|
114
119
|
geneId,
|
|
115
120
|
assetId,
|
|
116
121
|
signalsMatch,
|
|
117
122
|
view: emptyLearningView(geneId),
|
|
118
123
|
reuseCount: 0,
|
|
124
|
+
generationSource: gsrc,
|
|
119
125
|
...(category ? { category } : {}),
|
|
120
126
|
...(summary ? { summary } : {}),
|
|
121
127
|
});
|
|
@@ -145,7 +151,7 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
|
|
|
145
151
|
if (opts.hubCandidates && opts.hubCandidates.length > 0) {
|
|
146
152
|
const localIds = new Set(out.map((c) => c.geneId));
|
|
147
153
|
for (const h of opts.hubCandidates) {
|
|
148
|
-
if (!passesInjectedCandidateGates(h, opts))
|
|
154
|
+
if (!passesInjectedCandidateGates(h, opts, provenance, review))
|
|
149
155
|
continue;
|
|
150
156
|
if (localIds.has(h.geneId))
|
|
151
157
|
continue; // a trusted local gene already covers this id
|
|
@@ -160,7 +166,10 @@ export async function assembleSelectionPool(store, signals, opts = {}) {
|
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
168
|
for (const a of await store.list('AntiGene', limit)) {
|
|
163
|
-
|
|
169
|
+
// AntiGene is negative memory that changes an autonomous prompt. Unlike legacy/cycle-authored Genes, it must
|
|
170
|
+
// never inherit ReviewLedger's backward-compatible "no record = approved" default: no ledger, no record,
|
|
171
|
+
// quarantined, and rejected all fail closed. Only an explicit human approval can enable warning injection.
|
|
172
|
+
if (review?.get(String(a.asset_id))?.state !== 'approved')
|
|
164
173
|
continue;
|
|
165
174
|
const trigger = asStrings(a['trigger']);
|
|
166
175
|
const avoid = asStrings(a['avoid']);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ConversationCapabilityHit } from './conversationSniffer.js';
|
|
1
2
|
/** The "five-question shape" of a capability proposal (ported verbatim from v1). */
|
|
2
3
|
export interface FiveQuestionShape {
|
|
3
4
|
title: string;
|
|
@@ -30,6 +31,7 @@ export interface ExtractCandidatesInput {
|
|
|
30
31
|
toolCalls?: readonly string[];
|
|
31
32
|
signals?: readonly string[];
|
|
32
33
|
failedCapsules?: readonly FailedCapsuleLike[];
|
|
34
|
+
conversationHits?: readonly ConversationCapabilityHit[];
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
37
|
* Mine capability candidates from three sources (ported v1): repeated tool calls (>=3), active opportunity
|
|
@@ -80,6 +80,11 @@ export function extractCapabilityCandidates(inp) {
|
|
|
80
80
|
const title = `Repeated tool usage: ${tool}`;
|
|
81
81
|
out.push(candidate('transcript', stableHash(title), title, signals, `Observed ${count} occurrences of tool call ${tool}.`, signalTags));
|
|
82
82
|
}
|
|
83
|
+
// A2. Conversation sniffer hits: a transcript explicitly proved a reusable capability.
|
|
84
|
+
for (const hit of inp.conversationHits ?? []) {
|
|
85
|
+
const hitSignals = [...new Set([...(hit.signals ?? []), ...signals].map((s) => String(s)).filter(Boolean))];
|
|
86
|
+
out.push(candidate('transcript', stableHash('conversation:' + hit.id), hit.title || 'Verified reusable conversation capability', hitSignals, hit.evidence.length > 0 ? hit.evidence.slice(0, 2).join('; ') : hit.summary, [...new Set([...signalTags, ...expandSignals(hitSignals)])]));
|
|
87
|
+
}
|
|
83
88
|
// B. Signal-mapped candidates: predefined defensive/opportunity signals.
|
|
84
89
|
for (const sc of SIGNAL_CANDIDATES) {
|
|
85
90
|
if (!signals.some((s) => s === sc.signal || s.startsWith(sc.signal + ':')))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SignalSourceTurn } from '../signals/extractor.js';
|
|
2
|
+
export interface ConversationSnifferTurn extends SignalSourceTurn {
|
|
3
|
+
role?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface ConversationCapabilityHit {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
summary: string;
|
|
9
|
+
signals: string[];
|
|
10
|
+
evidence: string[];
|
|
11
|
+
score: number;
|
|
12
|
+
reasons: string[];
|
|
13
|
+
toolCalls: string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ConversationSnifferOptions {
|
|
16
|
+
maxHits?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function sniffConversationCapabilities(turns: readonly ConversationSnifferTurn[], opts?: ConversationSnifferOptions): ConversationCapabilityHit[];
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { redactString } from '../hub/sanitize.js';
|
|
3
|
+
const REUSABLE_RE = /\b(reusable|repeatable|workflow|playbook|runbook|capability|procedure|pattern|recipe|documented|future runs?|next time|can reuse|reuse this)\b|可复用|复用|能力|流程|工作流/i;
|
|
4
|
+
const PROOF_RE = /\b(validated|verified|passed|green|success(?:ful|fully)?|succeeded|works?|completed|published|uploaded|recorded:true|exit code:?\s*0|all tests passed)\b|验证|通过|成功|已发布|可用/i;
|
|
5
|
+
const FAILURE_RE = /\b(failed|failure|error|exception|traceback|exit code:?\s*[1-9]|not working|unable to)\b|失败|错误|报错/i;
|
|
6
|
+
const EXIT_ZERO_RE = /\bexit code:?\s*0\b/i;
|
|
7
|
+
const EXIT_NON_ZERO_RE = /\bexit code:?\s*[1-9]\d*\b/i;
|
|
8
|
+
const DOMAIN_TOKENS = [
|
|
9
|
+
[/\b(lark|feishu|飞书)\b/i, 'lark'],
|
|
10
|
+
[/\b(github|gh|pull request|bugbot)\b/i, 'github'],
|
|
11
|
+
[/\b(playwright|screenshot|visual)\b/i, 'playwright'],
|
|
12
|
+
[/\b(vitest|jest|test)\b/i, 'test'],
|
|
13
|
+
[/\b(pnpm|npm|yarn)\b/i, 'package-manager'],
|
|
14
|
+
[/\b(hub|publish|marketplace)\b/i, 'hub-publish'],
|
|
15
|
+
[/\b(release|checksum|sha256)\b/i, 'release'],
|
|
16
|
+
[/\b(api|http|endpoint)\b/i, 'api'],
|
|
17
|
+
[/\b(document|docs?|markdown)\b/i, 'docs'],
|
|
18
|
+
];
|
|
19
|
+
function cleanText(value, max = 220) {
|
|
20
|
+
const text = redactString(String(value ?? '').replace(/\s+/g, ' ').trim());
|
|
21
|
+
return text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
22
|
+
}
|
|
23
|
+
function turnText(turn) {
|
|
24
|
+
return cleanText([turn.text, turn.toolResult, turn.errorMessage].filter(Boolean).join('\n'), 500);
|
|
25
|
+
}
|
|
26
|
+
function stableId(parts) {
|
|
27
|
+
return `conversation_capability_${createHash('sha1').update(JSON.stringify(parts), 'utf8').digest('hex').slice(0, 12)}`;
|
|
28
|
+
}
|
|
29
|
+
function normalizeSignal(value) {
|
|
30
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '').slice(0, 48);
|
|
31
|
+
}
|
|
32
|
+
function collectToolCalls(turns) {
|
|
33
|
+
const seen = new Set();
|
|
34
|
+
for (const turn of turns) {
|
|
35
|
+
const tool = cleanText(turn.toolName ?? '', 80);
|
|
36
|
+
if (tool)
|
|
37
|
+
seen.add(tool);
|
|
38
|
+
}
|
|
39
|
+
return [...seen].slice(0, 8);
|
|
40
|
+
}
|
|
41
|
+
function domainSignals(text, toolCalls) {
|
|
42
|
+
const out = new Set();
|
|
43
|
+
for (const [re, signal] of DOMAIN_TOKENS) {
|
|
44
|
+
if (re.test(text))
|
|
45
|
+
out.add(signal);
|
|
46
|
+
}
|
|
47
|
+
for (const tool of toolCalls) {
|
|
48
|
+
const normalized = normalizeSignal(tool);
|
|
49
|
+
if (normalized)
|
|
50
|
+
out.add(normalized);
|
|
51
|
+
}
|
|
52
|
+
return [...out].slice(0, 6);
|
|
53
|
+
}
|
|
54
|
+
function isToolTurn(turn) {
|
|
55
|
+
return turn.role === 'tool' || Boolean(turn.toolName);
|
|
56
|
+
}
|
|
57
|
+
function classifyToolOutcome(turn) {
|
|
58
|
+
const text = turnText(turn);
|
|
59
|
+
if (EXIT_NON_ZERO_RE.test(text))
|
|
60
|
+
return 'failure';
|
|
61
|
+
if (EXIT_ZERO_RE.test(text))
|
|
62
|
+
return 'success';
|
|
63
|
+
if (PROOF_RE.test(text) && !FAILURE_RE.test(text))
|
|
64
|
+
return 'success';
|
|
65
|
+
if (FAILURE_RE.test(text))
|
|
66
|
+
return 'failure';
|
|
67
|
+
return 'unknown';
|
|
68
|
+
}
|
|
69
|
+
function hasTerminalSuccessfulToolRun(turns) {
|
|
70
|
+
let lastDecisive = 'unknown';
|
|
71
|
+
for (const turn of turns) {
|
|
72
|
+
if (!isToolTurn(turn))
|
|
73
|
+
continue;
|
|
74
|
+
const outcome = classifyToolOutcome(turn);
|
|
75
|
+
if (outcome !== 'unknown')
|
|
76
|
+
lastDecisive = outcome;
|
|
77
|
+
}
|
|
78
|
+
return lastDecisive === 'success';
|
|
79
|
+
}
|
|
80
|
+
function pickEvidence(turns, matcher, max = 3) {
|
|
81
|
+
const evidence = [];
|
|
82
|
+
for (const turn of turns) {
|
|
83
|
+
const text = turnText(turn);
|
|
84
|
+
if (!text || !matcher.test(text))
|
|
85
|
+
continue;
|
|
86
|
+
evidence.push(cleanText(text, 180));
|
|
87
|
+
if (evidence.length >= max)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
return evidence;
|
|
91
|
+
}
|
|
92
|
+
function pickSummary(turns, reusableEvidence, proofEvidence) {
|
|
93
|
+
const assistant = turns
|
|
94
|
+
.filter((turn) => turn.role === 'assistant' && !turn.isMeta)
|
|
95
|
+
.map(turnText)
|
|
96
|
+
.find((text) => text.length >= 40 && (REUSABLE_RE.test(text) || PROOF_RE.test(text)));
|
|
97
|
+
return cleanText(assistant ?? reusableEvidence[0] ?? proofEvidence[0] ?? 'Verified reusable conversation capability.', 180);
|
|
98
|
+
}
|
|
99
|
+
export function sniffConversationCapabilities(turns, opts = {}) {
|
|
100
|
+
const usableTurns = turns.filter((turn) => !turn.isMeta);
|
|
101
|
+
if (usableTurns.length === 0)
|
|
102
|
+
return [];
|
|
103
|
+
const allText = usableTurns.map(turnText).filter(Boolean).join('\n');
|
|
104
|
+
if (!allText.trim())
|
|
105
|
+
return [];
|
|
106
|
+
const reusableEvidence = pickEvidence(usableTurns, REUSABLE_RE);
|
|
107
|
+
const proofEvidence = pickEvidence(usableTurns, PROOF_RE);
|
|
108
|
+
if (reusableEvidence.length === 0 || proofEvidence.length === 0)
|
|
109
|
+
return [];
|
|
110
|
+
const failureOnly = FAILURE_RE.test(allText) && !PROOF_RE.test(allText);
|
|
111
|
+
if (failureOnly)
|
|
112
|
+
return [];
|
|
113
|
+
if (!hasTerminalSuccessfulToolRun(usableTurns))
|
|
114
|
+
return [];
|
|
115
|
+
const toolCalls = collectToolCalls(usableTurns);
|
|
116
|
+
const domain = domainSignals(allText, toolCalls);
|
|
117
|
+
const signals = [...new Set(['reusable_capability', 'verified_workflow', ...domain])].slice(0, 8);
|
|
118
|
+
const score = 4 + Math.min(2, reusableEvidence.length) + Math.min(2, proofEvidence.length) + Math.min(2, toolCalls.length);
|
|
119
|
+
const summary = pickSummary(usableTurns, reusableEvidence, proofEvidence);
|
|
120
|
+
const titleSignal = domain[0] ?? 'verified workflow';
|
|
121
|
+
const hit = {
|
|
122
|
+
id: stableId({ summary, signals, evidence: [...reusableEvidence, ...proofEvidence].slice(0, 4) }),
|
|
123
|
+
title: `Verified reusable capability: ${titleSignal}`,
|
|
124
|
+
summary,
|
|
125
|
+
signals,
|
|
126
|
+
evidence: [...reusableEvidence, ...proofEvidence].slice(0, 4),
|
|
127
|
+
score,
|
|
128
|
+
reasons: ['reusable_marker', 'proof_marker', ...(toolCalls.length > 0 ? ['tool_evidence'] : [])],
|
|
129
|
+
toolCalls,
|
|
130
|
+
};
|
|
131
|
+
return [hit].slice(0, opts.maxHits ?? 1);
|
|
132
|
+
}
|
|
@@ -10,11 +10,13 @@ import type { PersonalityStore } from '../personality/store.js';
|
|
|
10
10
|
import { type ResolutionStatus } from './solidify.js';
|
|
11
11
|
import type { ProofOfWork } from '../schema/proofOfWork.js';
|
|
12
12
|
import { type ClassifyRecentEvent } from './cycleFailureClassifier.js';
|
|
13
|
+
import type { MemoryGraphGeneEvidence } from './memoryGraph.js';
|
|
13
14
|
export interface TriggerEval {
|
|
14
15
|
trigger: boolean;
|
|
15
16
|
reasons: string[];
|
|
16
17
|
valueScore: number;
|
|
17
18
|
}
|
|
19
|
+
export type ExecutionFailureKind = 'spawn_failed' | 'timeout' | 'cancelled' | 'permission_denied' | 'non_zero_exit' | 'invalid_output' | 'runtime_error';
|
|
18
20
|
export interface ExecutionResult {
|
|
19
21
|
outcome: {
|
|
20
22
|
status: 'success' | 'failed';
|
|
@@ -23,6 +25,9 @@ export interface ExecutionResult {
|
|
|
23
25
|
};
|
|
24
26
|
proofOfWork?: ProofOfWork;
|
|
25
27
|
strongEvidence?: boolean;
|
|
28
|
+
/** Structured runner failure metadata. Safe to persist; unlike sessionLog it contains no transcript text. */
|
|
29
|
+
failureKind?: ExecutionFailureKind;
|
|
30
|
+
exitCode?: number | null;
|
|
26
31
|
/**
|
|
27
32
|
* The agent run's transcript (stdout, plus stderr when present), attached by the execution layer on a FAILED
|
|
28
33
|
* outcome. It is the host-side context classifyCycleFailure needs to reach the host_no_transcript /
|
|
@@ -102,6 +107,8 @@ export interface CycleInput {
|
|
|
102
107
|
distilledFallback?: readonly GeneCandidateInput[];
|
|
103
108
|
/** Advisory-only AntiGene warnings matched upstream for this cycle's base signals. */
|
|
104
109
|
antiWarnings?: readonly AntiWarning[];
|
|
110
|
+
/** Bounded structured outcome evidence for explainability and prompt enrichment. */
|
|
111
|
+
memoryEvidence?: readonly MemoryGraphGeneEvidence[];
|
|
105
112
|
/**
|
|
106
113
|
* Optional triage context for the cycle.failed event (PORT v1 #279 issue-reporter half). When the caller can
|
|
107
114
|
* supply a session transcript or a list of recent failed cycles, classifyCycleFailure runs and the resulting
|
|
@@ -125,11 +132,15 @@ export interface CycleResult {
|
|
|
125
132
|
cycleId: string;
|
|
126
133
|
triggered: boolean;
|
|
127
134
|
finalStage: CycleStage;
|
|
135
|
+
/** Real CycleEngine returns this on every path; optional keeps injected legacy engines source-compatible. */
|
|
136
|
+
producedValue?: boolean;
|
|
128
137
|
decision?: GeneDecision;
|
|
129
138
|
mutation?: Mutation;
|
|
130
139
|
capsule?: Capsule;
|
|
131
140
|
event?: EvolutionEvent;
|
|
132
141
|
resolutionStatus?: ResolutionStatus;
|
|
142
|
+
failureKind?: ExecutionFailureKind;
|
|
143
|
+
exitCode?: number | null;
|
|
133
144
|
reasons: string[];
|
|
134
145
|
}
|
|
135
146
|
/**
|
package/dist/algo/cycleEngine.js
CHANGED
|
@@ -183,6 +183,8 @@ export class CycleEngine {
|
|
|
183
183
|
// Locality must recognise V2's real gene prefix `gene_distilled_` (isDistilledGeneId), not just the
|
|
184
184
|
// V1-only `gene_auto_` / content-hash `sha256:` forms — otherwise this gate stays shut for every real V2
|
|
185
185
|
// gene and classifyCycleFailure never runs (mirror of isLocalGeneratedGene in cycleFailureClassifier.ts).
|
|
186
|
+
// NB the prefix is now a NAMESPACE marker, not a provenance tag (the authoritative source is generation_meta,
|
|
187
|
+
// V1 #302); this call site only has a gene id, so it falls back to the prefix.
|
|
186
188
|
const localCurrentGene = current?.geneId !== undefined && (isDistilledGeneId(current.geneId) || /^sha256:/.test(current.geneId) || /^gene_auto_/.test(current.geneId));
|
|
187
189
|
// Run classification when we have ANY host context (an exec transcript or an injected failureContext) OR a
|
|
188
190
|
// local-gene blast to judge; otherwise stay default-open (unclassified, no payload).
|
|
@@ -198,6 +200,10 @@ export class CycleEngine {
|
|
|
198
200
|
});
|
|
199
201
|
return r.reason ? { failure_class: r.failureClass, failure_class_reason: r.reason } : { failure_class: r.failureClass };
|
|
200
202
|
};
|
|
203
|
+
const executionMetadata = (exec) => ({
|
|
204
|
+
...(exec.failureKind !== undefined ? { failureKind: exec.failureKind } : {}),
|
|
205
|
+
...(exec.exitCode !== undefined ? { exitCode: exec.exitCode } : {}),
|
|
206
|
+
});
|
|
201
207
|
const strategy = resolveStrategy({ name: input.strategyName, signals: cycleSignals, cycleCount: historyCycleCount(ingestor, 1000, input.cycleId) });
|
|
202
208
|
const cycleCategory = categoryForStrategy(input.category, strategy);
|
|
203
209
|
let stage = 'none';
|
|
@@ -224,7 +230,7 @@ export class CycleEngine {
|
|
|
224
230
|
if (!trig.trigger) {
|
|
225
231
|
await ingestor.ingest({ type: 'cycle.aborted', human: { title: `cycle ${input.cycleId} 抑制` }, payload: { cycleId: input.cycleId, reasons: trig.reasons } });
|
|
226
232
|
advance('aborted');
|
|
227
|
-
return { cycleId: input.cycleId, triggered: false, finalStage: stage, reasons: trig.reasons };
|
|
233
|
+
return { cycleId: input.cycleId, triggered: false, finalStage: stage, producedValue: false, reasons: trig.reasons };
|
|
228
234
|
}
|
|
229
235
|
// 选 gene(可解释决策). Exploration: when recent cycles plateau, enable drift to escape local optima.
|
|
230
236
|
const recentOutcomes = recentCycleOutcomes(ingestor, 100);
|
|
@@ -295,11 +301,11 @@ export class CycleEngine {
|
|
|
295
301
|
const epi = epigeneticPenaltyForIds(candidateIds(c), envKey, geneOutcomes);
|
|
296
302
|
return epi > 0 ? { ...c, epigeneticPenalty: epi } : c;
|
|
297
303
|
});
|
|
298
|
-
const decision = (await this.deps.selection.run({ signals: baseSignals, candidates, floor: input.selectionFloor, ...(input.forcedGeneId !== undefined ? { forcedGeneId: input.forcedGeneId } : {}), ...(exploration ? { exploration } : {}), ...(distilledFallback.length > 0 ? { distilledFallback } : {}), ...(input.antiWarnings && input.antiWarnings.length > 0 ? { antiWarnings: input.antiWarnings } : {}) }, { now, cycleId: input.cycleId, ...(this.deps.rng ? { rng: this.deps.rng } : {}) }));
|
|
304
|
+
const decision = (await this.deps.selection.run({ signals: baseSignals, candidates, floor: input.selectionFloor, ...(input.forcedGeneId !== undefined ? { forcedGeneId: input.forcedGeneId } : {}), ...(exploration ? { exploration } : {}), ...(distilledFallback.length > 0 ? { distilledFallback } : {}), ...(input.antiWarnings && input.antiWarnings.length > 0 ? { antiWarnings: input.antiWarnings } : {}), ...(input.memoryEvidence && input.memoryEvidence.length > 0 ? { memoryEvidence: input.memoryEvidence } : {}) }, { now, cycleId: input.cycleId, ...(this.deps.rng ? { rng: this.deps.rng } : {}) }));
|
|
299
305
|
await ingestor.ingest({
|
|
300
306
|
type: 'decision.gene_selected',
|
|
301
307
|
human: { title: `选 gene ${decision.selectedGeneId ?? '(innovate)'}`, why: decision.candidates.map((c) => `${c.geneId}:${c.score.toFixed(3)}`).join(', ') || '无候选→innovate' },
|
|
302
|
-
payload: { cycleId: input.cycleId, selectedGeneId: decision.selectedGeneId, ...(decision.selectedAssetId ? { selectedAssetId: decision.selectedAssetId } : {}), candidates: decision.candidates, ...(decision.antiWarnings && decision.antiWarnings.length > 0 ? { antiWarnings: decision.antiWarnings } : {}), weightsVersion: decision.weightsVersion, strategy: decision.strategyName, strategyPreset: strategy.name, ...(plateau.active ? { plateau } : {}), ...(inertBanned.size > 0 ? { inertBanned: [...inertBanned] } : {}) },
|
|
308
|
+
payload: { cycleId: input.cycleId, selectedGeneId: decision.selectedGeneId, ...(decision.selectedAssetId ? { selectedAssetId: decision.selectedAssetId } : {}), ...(decision.selectedReason ? { selectedReason: decision.selectedReason } : {}), candidates: decision.candidates, ...(decision.antiWarnings && decision.antiWarnings.length > 0 ? { antiWarnings: decision.antiWarnings } : {}), ...(decision.memoryEvidence && decision.memoryEvidence.length > 0 ? { memoryEvidence: decision.memoryEvidence } : {}), weightsVersion: decision.weightsVersion, strategy: decision.strategyName, strategyPreset: strategy.name, ...(plateau.active ? { plateau } : {}), ...(inertBanned.size > 0 ? { inertBanned: [...inertBanned] } : {}) },
|
|
303
309
|
});
|
|
304
310
|
advance('gene_selected');
|
|
305
311
|
const geneId = decision.selectedGeneId ?? 'ad-hoc';
|
|
@@ -329,7 +335,7 @@ export class CycleEngine {
|
|
|
329
335
|
await ingestor.ingest({ type: 'cycle.failed', human: { title: `cycle ${input.cycleId} 执行抛错` }, payload: { cycleId: input.cycleId, error: msg, gene: geneId, env: envKey, ...failureClassPayload({ geneId }, msg) } });
|
|
330
336
|
advance('failed');
|
|
331
337
|
await recordPersonalityOutcome('failed', null);
|
|
332
|
-
return { cycleId: input.cycleId, triggered: true, finalStage: stage, decision, mutation, reasons: [...reasons, `执行异常: ${msg}`] };
|
|
338
|
+
return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: false, decision, mutation, reasons: [...reasons, `执行异常: ${msg}`] };
|
|
333
339
|
}
|
|
334
340
|
if (input.solidifyPermit) {
|
|
335
341
|
let permit;
|
|
@@ -358,12 +364,13 @@ export class CycleEngine {
|
|
|
358
364
|
reason: permit.reason,
|
|
359
365
|
gene: geneId,
|
|
360
366
|
env: envKey,
|
|
367
|
+
...executionMetadata(exec),
|
|
361
368
|
...failureClassPayload({ geneId }),
|
|
362
369
|
},
|
|
363
370
|
});
|
|
364
371
|
advance('failed');
|
|
365
372
|
await recordPersonalityOutcome('failed', null);
|
|
366
|
-
return { cycleId: input.cycleId, triggered: true, finalStage: stage, decision, mutation, reasons: [...reasons, reason] };
|
|
373
|
+
return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: false, decision, mutation, ...executionMetadata(exec), reasons: [...reasons, reason] };
|
|
367
374
|
}
|
|
368
375
|
}
|
|
369
376
|
// solidify → Capsule
|
|
@@ -392,6 +399,7 @@ export class CycleEngine {
|
|
|
392
399
|
outcome: exec.outcome,
|
|
393
400
|
gene: geneId,
|
|
394
401
|
env: envKey,
|
|
402
|
+
...executionMetadata(exec),
|
|
395
403
|
// Only forward blast radius as a no-op signal when it was measured from a git_diff proof. solidify.ts
|
|
396
404
|
// forces blast to {0,0} for non-git_diff proofs (artifact_hash/external_receipt/tool_call_trace),
|
|
397
405
|
// where {0,0} means "blast unknown", NOT "no change" — forwarding it would mis-classify a productive
|
|
@@ -412,6 +420,6 @@ export class CycleEngine {
|
|
|
412
420
|
}
|
|
413
421
|
// 人格统计回写(用途②): 用本轮真实 outcome/score 回写当轮人格桶, 供下一轮自然选择靠拢. 未注入 ⇒ no-op.
|
|
414
422
|
await recordPersonalityOutcome(exec.outcome.status, exec.outcome.score);
|
|
415
|
-
return { cycleId: input.cycleId, triggered: true, finalStage: stage, decision, mutation, capsule: sol.capsule, event, resolutionStatus: sol.resolutionStatus, reasons };
|
|
423
|
+
return { cycleId: input.cycleId, triggered: true, finalStage: stage, producedValue: sol.producedValue, decision, mutation, capsule: sol.capsule, event, resolutionStatus: sol.resolutionStatus, ...executionMetadata(exec), reasons };
|
|
416
424
|
}
|
|
417
425
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Bucket the failure falls into. unclassified = default-open (treat as a real evolver bug worth filing).
|
|
2
|
-
* Bucket string matches V1
|
|
2
|
+
* Bucket string matches V1 byte-for-byte so wire / UI
|
|
3
3
|
* consumers can branch on the same value regardless of producer. */
|
|
4
4
|
export type CycleFailureClass = 'host_no_transcript' | 'host_provider_error' | 'local_gene_no_blast' | 'unclassified';
|
|
5
5
|
/** Shape of a recent cycle event the classifier reads. Loose so callers can pass partial DTOs. */
|
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
import { isDistilledGeneId } from './geneIntake.js';
|
|
13
13
|
// "[NO SESSION LOGS FOUND]" et al. — the host gave evolver nothing to evolve from.
|
|
14
14
|
const HOST_NO_TRANSCRIPT_RE = /\[no session logs? found\]|no session log|no transcript/i;
|
|
15
|
+
// Interactive runner gates can stop execution before the provider is invoked. They still belong to the
|
|
16
|
+
// host_provider_error compatibility bucket because the local gene never had a chance to run.
|
|
17
|
+
const HOST_RUNNER_ERR_RE = /Workspace Trust Required/i;
|
|
15
18
|
// Host LLM-provider errors. None of these strings are emitted by evolver core (v1 #279 verified the same;
|
|
16
19
|
// keeping the regex byte-for-byte equivalent), so their presence means the host Agent's provider call failed.
|
|
17
|
-
const HOST_PROVIDER_ERR_RE = /\bLLM ERROR\b|\bMaxTokens\b|field MaxTokens|max_tokens[^\n]{0,24}invalid|insufficient_quota|invalid_api_key|\brate limit(?:ed| exceeded)?\b|quota exceeded|context length exceeded|maximum context length/i;
|
|
20
|
+
const HOST_PROVIDER_ERR_RE = /\bLLM ERROR\b|\bMaxTokens\b|field MaxTokens|max_tokens[^\n]{0,24}invalid|insufficient_quota|invalid_api_key|\brate limit(?:ed| exceeded)?\b|quota exceeded|context length exceeded|maximum context length|disabled Claude subscription access|Use an Anthropic API key|not authenticated|authentication required|login required|not logged in/i;
|
|
18
21
|
// Locality prefixes a "locally-generated" gene id can carry:
|
|
19
|
-
// - `gene_distilled_` — the
|
|
20
|
-
// surfaced via isDistilledGeneId).
|
|
21
|
-
//
|
|
22
|
+
// - `gene_distilled_` — the namespace V2 mints for every intaken gene (geneIntake.DISTILLED_ID_PREFIX,
|
|
23
|
+
// surfaced via isDistilledGeneId). NB this is a NAMESPACE marker now, not a provenance tag: the authoritative
|
|
24
|
+
// source is generation_meta (V1 #302, read via geneGenerationSource), but this call site only has a gene id, so
|
|
25
|
+
// it falls back to the prefix. Without this the whole `local_gene_no_blast` bucket never fired for real V2 genes.
|
|
22
26
|
// - `gene_auto_` — V1-only legacy form (V2 never produces it); kept so V1-shaped inputs still match.
|
|
23
27
|
// - `sha256:` — a content asset_id (not a logical gene id); kept for back-compat / loose callers.
|
|
24
28
|
function isLocalGeneratedGene(geneId) {
|
|
@@ -60,7 +64,11 @@ export function classifyCycleFailure(opts) {
|
|
|
60
64
|
if (hasSessionLog && (!log.trim() || HOST_NO_TRANSCRIPT_RE.test(log))) {
|
|
61
65
|
return { failureClass: 'host_no_transcript', reason: 'no session transcript to evolve from' };
|
|
62
66
|
}
|
|
63
|
-
// (b) Host LLM-provider error surfaced in the transcript.
|
|
67
|
+
// (b) Host runner preflight or LLM-provider error surfaced in the transcript.
|
|
68
|
+
const runner = hasSessionLog ? log.match(HOST_RUNNER_ERR_RE) : null;
|
|
69
|
+
if (runner) {
|
|
70
|
+
return { failureClass: 'host_provider_error', reason: `host runner error: ${String(runner[0]).slice(0, 60)}` };
|
|
71
|
+
}
|
|
64
72
|
const prov = hasSessionLog ? log.match(HOST_PROVIDER_ERR_RE) : null;
|
|
65
73
|
if (prov) {
|
|
66
74
|
return { failureClass: 'host_provider_error', reason: `host LLM-provider error: ${String(prov[0]).slice(0, 60)}` };
|
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { type Gene } from '../wire/index.js';
|
|
1
|
+
import { type Gene, type GenerationSource } from '../wire/index.js';
|
|
2
2
|
/**
|
|
3
|
-
* True when a gene id is
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* True when a gene id is in the `gene_distilled_` namespace. NB this prefix is now a NAMESPACE marker, NOT a
|
|
4
|
+
* provenance tag: v1 used it to mean "skill-derived" (auto-evolved genes were `gene_auto_`), but v2's intakeGene
|
|
5
|
+
* tags EVERY intaken gene this way regardless of origin. The authoritative provenance is {@link geneGenerationSource};
|
|
6
|
+
* this prefix check remains as a back-compat fallback for legacy pooled genes that predate generation_meta, and for
|
|
7
|
+
* call sites that only have a gene id (not the full record).
|
|
6
8
|
*/
|
|
7
9
|
export declare function isDistilledGeneId(id: string | undefined | null): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The authoritative gene-generation source: read `generation_meta.source` from a gene-shaped record (V1 #302). Falls
|
|
12
|
+
* back to the id-prefix namespace when the record carries no generation_meta (legacy pooled genes predate the field),
|
|
13
|
+
* returning 'distilled' for a `gene_distilled_` id and 'manual' otherwise — so the fallback never claims a higher
|
|
14
|
+
* provenance tier than the prefix can vouch for. `null` means "undetermined" (no record, no recognizable id).
|
|
15
|
+
*/
|
|
16
|
+
export declare function geneGenerationSource(gene: Record<string, unknown> | null | undefined, geneId?: string | null): GenerationSource | null;
|
|
8
17
|
export interface GeneCandidate {
|
|
9
18
|
id?: string;
|
|
10
19
|
category?: string;
|
|
@@ -19,6 +28,7 @@ export interface GeneCandidate {
|
|
|
19
28
|
validation?: readonly string[];
|
|
20
29
|
routing_hint?: unknown;
|
|
21
30
|
tool_policy?: unknown;
|
|
31
|
+
generation_meta?: unknown;
|
|
22
32
|
}
|
|
23
33
|
/** Minimal shape of an existing gene needed for dedup. */
|
|
24
34
|
export interface ExistingGeneRef {
|
package/dist/algo/geneIntake.js
CHANGED
|
@@ -5,20 +5,44 @@
|
|
|
5
5
|
// signals fully overlap an existing one (dedup, so the pool does not grow redundant). The gene's CONTENT
|
|
6
6
|
// is generated upstream (the agent runtime / a distillation prompt — generative, out of core scope); this
|
|
7
7
|
// slice is the structural gate. Pairs with capabilityCandidates (which proposes what to distill).
|
|
8
|
-
import { computeAssetId, validateWire, SCHEMA_VERSION, normalizeRoutingHint, normalizeToolPolicy, stripGeneHints, } from '../wire/index.js';
|
|
8
|
+
import { computeAssetId, validateWire, SCHEMA_VERSION, normalizeRoutingHint, normalizeToolPolicy, normalizeGenerationMeta, stripGeneHints, } from '../wire/index.js';
|
|
9
9
|
const VALID_CATEGORIES = ['repair', 'optimize', 'innovate', 'explore'];
|
|
10
10
|
const DEFAULT_FORBIDDEN_PATHS = ['.git', 'node_modules'];
|
|
11
11
|
const DEFAULT_MAX_FILES = 12;
|
|
12
12
|
/** Id prefix every distilled (skill-derived) gene carries. Recognised via {@link isDistilledGeneId}. */
|
|
13
13
|
const DISTILLED_ID_PREFIX = 'gene_distilled_';
|
|
14
14
|
/**
|
|
15
|
-
* True when a gene id is
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* True when a gene id is in the `gene_distilled_` namespace. NB this prefix is now a NAMESPACE marker, NOT a
|
|
16
|
+
* provenance tag: v1 used it to mean "skill-derived" (auto-evolved genes were `gene_auto_`), but v2's intakeGene
|
|
17
|
+
* tags EVERY intaken gene this way regardless of origin. The authoritative provenance is {@link geneGenerationSource};
|
|
18
|
+
* this prefix check remains as a back-compat fallback for legacy pooled genes that predate generation_meta, and for
|
|
19
|
+
* call sites that only have a gene id (not the full record).
|
|
18
20
|
*/
|
|
19
21
|
export function isDistilledGeneId(id) {
|
|
20
22
|
return typeof id === 'string' && id.startsWith(DISTILLED_ID_PREFIX);
|
|
21
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* The authoritative gene-generation source: read `generation_meta.source` from a gene-shaped record (V1 #302). Falls
|
|
26
|
+
* back to the id-prefix namespace when the record carries no generation_meta (legacy pooled genes predate the field),
|
|
27
|
+
* returning 'distilled' for a `gene_distilled_` id and 'manual' otherwise — so the fallback never claims a higher
|
|
28
|
+
* provenance tier than the prefix can vouch for. `null` means "undetermined" (no record, no recognizable id).
|
|
29
|
+
*/
|
|
30
|
+
export function geneGenerationSource(gene, geneId) {
|
|
31
|
+
const meta = gene?.['generation_meta'];
|
|
32
|
+
if (meta && typeof meta === 'object') {
|
|
33
|
+
const src = meta['source'];
|
|
34
|
+
if (typeof src === 'string' && (src === 'evolved' || src === 'distilled' || src === 'manual'))
|
|
35
|
+
return src;
|
|
36
|
+
}
|
|
37
|
+
// Legacy fallback: a `gene_distilled_` id is at best a distilled gene (v2 minted it for ALL intake sources, so it
|
|
38
|
+
// cannot be trusted as "evolved"); any other id is treated as manual (human-authored / externally seeded).
|
|
39
|
+
const id = geneId ?? (typeof gene?.['id'] === 'string' ? String(gene['id']) : null);
|
|
40
|
+
if (id && isDistilledGeneId(id))
|
|
41
|
+
return 'distilled';
|
|
42
|
+
if (id)
|
|
43
|
+
return 'manual';
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
22
46
|
function fnv1a(s) {
|
|
23
47
|
let h = 2166136261;
|
|
24
48
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -65,6 +89,9 @@ export function intakeGene(candidate, existing = []) {
|
|
|
65
89
|
// absent when malformed/empty. The router / tool-gate reads absent === "no opinion".
|
|
66
90
|
const routingHint = normalizeRoutingHint(candidate.routing_hint);
|
|
67
91
|
const toolPolicy = normalizeToolPolicy(candidate.tool_policy);
|
|
92
|
+
// v2-delta provenance + quality metadata (v1 #302): normalized so a candidate carrying it lands canonical,
|
|
93
|
+
// dropped to absent when malformed/no-recognized-source.
|
|
94
|
+
const generationMeta = normalizeGenerationMeta(candidate.generation_meta);
|
|
68
95
|
const gene = {
|
|
69
96
|
type: 'Gene',
|
|
70
97
|
schema_version: SCHEMA_VERSION,
|
|
@@ -81,16 +108,17 @@ export function intakeGene(candidate, existing = []) {
|
|
|
81
108
|
...(candidate.preconditions ? { preconditions: [...candidate.preconditions] } : {}),
|
|
82
109
|
...(routingHint ? { routing_hint: routingHint } : {}),
|
|
83
110
|
...(toolPolicy ? { tool_policy: toolPolicy } : {}),
|
|
111
|
+
...(generationMeta ? { generation_meta: generationMeta } : {}),
|
|
84
112
|
asset_id: '',
|
|
85
113
|
};
|
|
86
|
-
// asset_id folds in the hints (intake's own canonical shape — gep-sdk canonicalize hashes every
|
|
87
|
-
// NB: this is NOT byte-equal to what v1's createGene would hash for the same logical gene — v1 always emits
|
|
114
|
+
// asset_id folds in the hints + generation_meta (intake's own canonical shape — gep-sdk canonicalize hashes every
|
|
115
|
+
// own key). NB: this is NOT byte-equal to what v1's createGene would hash for the same logical gene — v1 always emits
|
|
88
116
|
// routing_hint/tool_policy:null plus empty epigenetic_marks/learning_history/anti_patterns/preconditions that
|
|
89
117
|
// v2 intake omits, so the canonical shapes differ. Self-consistent here; not a cross-impl parity guarantee.
|
|
90
118
|
gene.asset_id = computeAssetId(gene) ?? '';
|
|
91
|
-
// Structural gate validates the gep-sdk-known CORE. routing_hint / tool_policy are v2-delta
|
|
92
|
-
// gene-schema bump), so strip them before the check — else additionalProperties:false rejects the
|
|
93
|
-
// intake. The full hinted gene stays schema-invalid until the SDK catches up — surfaced by the advisory
|
|
119
|
+
// Structural gate validates the gep-sdk-known CORE. routing_hint / tool_policy / generation_meta are v2-delta
|
|
120
|
+
// (pending a gene-schema bump), so strip them before the check — else additionalProperties:false rejects the
|
|
121
|
+
// gene at intake. The full hinted gene stays schema-invalid until the SDK catches up — surfaced by the advisory
|
|
94
122
|
// validateWire preview (evolver_gep_build) and enforced by the hub on receipt, NOT by a local egress gate
|
|
95
123
|
// (the publish/egress path is sanitize-only). Same v2-delta contract Capsule's proof_of_work had pre-1.11.0.
|
|
96
124
|
const v = validateWire(stripGeneHints(gene));
|