dsh-continual-evolve 0.10.6 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -122
- package/README.zh.md +26 -97
- package/cordis.patch.yml +2 -2
- package/lib/index.d.ts +44 -241
- package/lib/index.js +30 -253
- package/lib/memory-guide.d.ts +13 -14
- package/lib/memory-guide.js +32 -21
- package/lib/memory-section.d.ts +77 -0
- package/lib/memory-section.js +189 -0
- package/package.json +2 -17
- package/lib/apply.d.ts +0 -22
- package/lib/apply.js +0 -157
- package/lib/approval.d.ts +0 -72
- package/lib/approval.js +0 -127
- package/lib/auto.d.ts +0 -236
- package/lib/auto.js +0 -773
- package/lib/autocase.d.ts +0 -38
- package/lib/autocase.js +0 -76
- package/lib/benchmark-command.d.ts +0 -9
- package/lib/benchmark-command.js +0 -357
- package/lib/benchmark.d.ts +0 -151
- package/lib/benchmark.js +0 -250
- package/lib/command.d.ts +0 -49
- package/lib/command.js +0 -568
- package/lib/consolidate.d.ts +0 -90
- package/lib/consolidate.js +0 -180
- package/lib/copy.d.ts +0 -74
- package/lib/copy.js +0 -160
- package/lib/declines.d.ts +0 -101
- package/lib/declines.js +0 -175
- package/lib/evaluate.d.ts +0 -74
- package/lib/evaluate.js +0 -271
- package/lib/evolve-event.d.ts +0 -40
- package/lib/evolve-event.js +0 -59
- package/lib/failures.d.ts +0 -42
- package/lib/failures.js +0 -171
- package/lib/fate.d.ts +0 -130
- package/lib/fate.js +0 -350
- package/lib/goal-command.d.ts +0 -7
- package/lib/goal-command.js +0 -37
- package/lib/goal.d.ts +0 -72
- package/lib/goal.js +0 -72
- package/lib/inject.d.ts +0 -196
- package/lib/inject.js +0 -412
- package/lib/llm-text.d.ts +0 -104
- package/lib/llm-text.js +0 -140
- package/lib/logfile.d.ts +0 -71
- package/lib/logfile.js +0 -159
- package/lib/memory-agent.d.ts +0 -129
- package/lib/memory-agent.js +0 -568
- package/lib/memory-benchmark.d.ts +0 -76
- package/lib/memory-benchmark.js +0 -170
- package/lib/memory-index.d.ts +0 -114
- package/lib/memory-index.js +0 -197
- package/lib/message-source.d.ts +0 -23
- package/lib/message-source.js +0 -12
- package/lib/mount-command.d.ts +0 -10
- package/lib/mount-command.js +0 -48
- package/lib/mount.d.ts +0 -42
- package/lib/mount.js +0 -212
- package/lib/notify.d.ts +0 -59
- package/lib/notify.js +0 -97
- package/lib/plan.d.ts +0 -16
- package/lib/plan.js +0 -126
- package/lib/planner.d.ts +0 -61
- package/lib/planner.js +0 -144
- package/lib/pool.d.ts +0 -7
- package/lib/pool.js +0 -25
- package/lib/prefix-cache.d.ts +0 -78
- package/lib/prefix-cache.js +0 -168
- package/lib/project.d.ts +0 -28
- package/lib/project.js +0 -66
- package/lib/projection.d.ts +0 -22
- package/lib/projection.js +0 -112
- package/lib/promotion.d.ts +0 -94
- package/lib/promotion.js +0 -171
- package/lib/recall.d.ts +0 -85
- package/lib/recall.js +0 -142
- package/lib/record-language.d.ts +0 -85
- package/lib/record-language.js +0 -152
- package/lib/render.d.ts +0 -13
- package/lib/render.js +0 -80
- package/lib/review-scheduler.d.ts +0 -38
- package/lib/review-scheduler.js +0 -191
- package/lib/review.d.ts +0 -60
- package/lib/review.js +0 -145
- package/lib/rollback.d.ts +0 -9
- package/lib/rollback.js +0 -68
- package/lib/rubric.d.ts +0 -29
- package/lib/rubric.js +0 -119
- package/lib/runtime.d.ts +0 -24
- package/lib/runtime.js +0 -87
- package/lib/score.d.ts +0 -64
- package/lib/score.js +0 -191
- package/lib/search.d.ts +0 -83
- package/lib/search.js +0 -136
- package/lib/service.d.ts +0 -49
- package/lib/service.js +0 -180
- package/lib/skill-render.d.ts +0 -23
- package/lib/skill-render.js +0 -69
- package/lib/skill.d.ts +0 -15
- package/lib/skill.js +0 -80
- package/lib/skillquality.d.ts +0 -85
- package/lib/skillquality.js +0 -320
- package/lib/source.d.ts +0 -30
- package/lib/source.js +0 -43
- package/lib/state.d.ts +0 -26
- package/lib/state.js +0 -151
- package/lib/store.d.ts +0 -62
- package/lib/store.js +0 -164
- package/lib/token-usage.d.ts +0 -68
- package/lib/token-usage.js +0 -211
- package/lib/tool.d.ts +0 -34
- package/lib/tool.js +0 -320
- package/lib/turn-snapshot.d.ts +0 -72
- package/lib/turn-snapshot.js +0 -238
- package/lib/types.d.ts +0 -208
- package/lib/types.js +0 -127
- package/lib/usage.d.ts +0 -36
- package/lib/usage.js +0 -98
- package/lib/validate.d.ts +0 -28
- package/lib/validate.js +0 -181
- package/lib/wrapup-command.d.ts +0 -10
- package/lib/wrapup-command.js +0 -243
- package/lib/wrapup.d.ts +0 -260
- package/lib/wrapup.js +0 -539
package/lib/failures.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Failure-signature aggregation (gap R/D1 — observation stage): turn the
|
|
3
|
-
* free-text failure records the system already produces (review-gate failures
|
|
4
|
-
* in reviews.jsonl, failed cells in benchmark scoreboards) into a structured
|
|
5
|
-
* count by failure CLASS. This is deliberately NOT the full failure-signature
|
|
6
|
-
* Refiner from gap D1 — no routing, no policy — it is the data layer that
|
|
7
|
-
* lets a later patch decide whether a given failure class recurs often enough
|
|
8
|
-
* to deserve one.
|
|
9
|
-
*
|
|
10
|
-
* Classes are extracted with pure prefix rules (see classifyFailure), so the
|
|
11
|
-
* aggregation is deterministic and unit-testable: the same failure text
|
|
12
|
-
* always lands in the same class.
|
|
13
|
-
*/
|
|
14
|
-
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
/**
|
|
17
|
-
* Classify a failure message by prefix rules. Deterministic and additive:
|
|
18
|
-
* unknown text falls into "other" so the summary never drops a failure.
|
|
19
|
-
*/
|
|
20
|
-
export function classifyFailure(message) {
|
|
21
|
-
const text = (message ?? "").trim();
|
|
22
|
-
const lower = text.toLowerCase();
|
|
23
|
-
if (lower.includes("rubric decrypt failed"))
|
|
24
|
-
return "rubric-decrypt";
|
|
25
|
-
if (lower.includes("materials changed"))
|
|
26
|
-
return "material-drift";
|
|
27
|
-
if (lower.includes("executor failed") || lower.includes("executor stopped"))
|
|
28
|
-
return "executor";
|
|
29
|
-
if (lower.includes("reviewer failed") || lower.includes("reviewer stopped"))
|
|
30
|
-
return "reviewer";
|
|
31
|
-
if (lower.includes("fate assessment error"))
|
|
32
|
-
return "fate-assessor";
|
|
33
|
-
if (lower.includes("trajectory unavailable"))
|
|
34
|
-
return "trajectory";
|
|
35
|
-
if (lower.includes("output budget exhausted") || lower.includes("max-tokens"))
|
|
36
|
-
return "max-tokens";
|
|
37
|
-
if (lower.includes("llm call aborted") || lower.includes("aborted"))
|
|
38
|
-
return "aborted";
|
|
39
|
-
if (lower.includes("llm call failed"))
|
|
40
|
-
return "llm";
|
|
41
|
-
if (lower.includes("gate error"))
|
|
42
|
-
return "gate";
|
|
43
|
-
if (lower.includes("casecheck") || lower.includes("case check"))
|
|
44
|
-
return "casecheck";
|
|
45
|
-
return "other";
|
|
46
|
-
}
|
|
47
|
-
/** Aggregate a record list into counts. Empty input yields an all-zero summary. */
|
|
48
|
-
export function summarizeFailures(records) {
|
|
49
|
-
const byKind = {};
|
|
50
|
-
const bySource = {};
|
|
51
|
-
for (const record of records) {
|
|
52
|
-
byKind[record.kind] = (byKind[record.kind] ?? 0) + 1;
|
|
53
|
-
bySource[record.source] = (bySource[record.source] ?? 0) + 1;
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
total: records.length,
|
|
57
|
-
byKind: Object.fromEntries(Object.entries(byKind).sort((a, b) => b[1] - a[1])),
|
|
58
|
-
bySource: Object.fromEntries(Object.entries(bySource).sort((a, b) => b[1] - a[1])),
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Read failed review-gate records from `<baseDir>/evolve/reviews.jsonl`
|
|
63
|
-
* (outcome === "failed"). Tolerant of a missing/corrupt file.
|
|
64
|
-
*/
|
|
65
|
-
export function readReviewFailures(baseDir) {
|
|
66
|
-
const path = join(baseDir, "evolve", "reviews.jsonl");
|
|
67
|
-
if (!existsSync(path))
|
|
68
|
-
return [];
|
|
69
|
-
const records = [];
|
|
70
|
-
try {
|
|
71
|
-
for (const line of readFileSync(path, "utf8").split("\n")) {
|
|
72
|
-
if (line.trim().length === 0)
|
|
73
|
-
continue;
|
|
74
|
-
try {
|
|
75
|
-
const raw = JSON.parse(line);
|
|
76
|
-
if (raw.outcome !== "failed" || !raw.rationale)
|
|
77
|
-
continue;
|
|
78
|
-
records.push({
|
|
79
|
-
...(raw.timestamp ? { timestamp: raw.timestamp } : {}),
|
|
80
|
-
source: `review-gate${raw.reason ? `:${raw.reason}` : ""}`,
|
|
81
|
-
kind: classifyFailure(raw.rationale),
|
|
82
|
-
message: raw.rationale,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
// skip malformed lines — the audit file must never break reporting
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return [];
|
|
92
|
-
}
|
|
93
|
-
return records;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Read failed cells from every benchmark scoreboard under
|
|
97
|
-
* `<baseDir>/evolve/benchmarks/<bid>/`. Tolerant of missing/corrupt data.
|
|
98
|
-
*/
|
|
99
|
-
export function readBenchmarkFailures(baseDir) {
|
|
100
|
-
const root = join(baseDir, "evolve", "benchmarks");
|
|
101
|
-
if (!existsSync(root))
|
|
102
|
-
return [];
|
|
103
|
-
const records = [];
|
|
104
|
-
let bids;
|
|
105
|
-
try {
|
|
106
|
-
bids = readdirSync(root);
|
|
107
|
-
}
|
|
108
|
-
catch {
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
for (const bid of bids) {
|
|
112
|
-
const boardPath = join(root, bid, "scoreboard.json");
|
|
113
|
-
if (!existsSync(boardPath))
|
|
114
|
-
continue;
|
|
115
|
-
let board;
|
|
116
|
-
try {
|
|
117
|
-
board = JSON.parse(readFileSync(boardPath, "utf8"));
|
|
118
|
-
}
|
|
119
|
-
catch {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const cellLists = [];
|
|
123
|
-
if (board.reference?.cells)
|
|
124
|
-
cellLists.push({ label: "reference", cells: board.reference.cells });
|
|
125
|
-
for (const entry of board.candidates ?? []) {
|
|
126
|
-
if (entry.cells)
|
|
127
|
-
cellLists.push({ label: entry.label ?? "candidate", cells: entry.cells });
|
|
128
|
-
}
|
|
129
|
-
for (const list of cellLists) {
|
|
130
|
-
for (const cell of list.cells) {
|
|
131
|
-
const c = cell;
|
|
132
|
-
if (c.status !== "failed" || !c.notes)
|
|
133
|
-
continue;
|
|
134
|
-
records.push({
|
|
135
|
-
source: `benchmark:${bid}:${c.caseId ?? "?"}`,
|
|
136
|
-
kind: classifyFailure(c.notes),
|
|
137
|
-
message: c.notes,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
return records;
|
|
143
|
-
}
|
|
144
|
-
/** Combine both sources into one summary. */
|
|
145
|
-
export function collectFailureSummary(baseDir) {
|
|
146
|
-
const records = [...readReviewFailures(baseDir), ...readBenchmarkFailures(baseDir)];
|
|
147
|
-
return { summary: summarizeFailures(records), records };
|
|
148
|
-
}
|
|
149
|
-
/** Human-readable report for the command line. */
|
|
150
|
-
export function formatFailureSummary(summary) {
|
|
151
|
-
const lines = [`failure summary: ${summary.total} total`];
|
|
152
|
-
const kinds = Object.entries(summary.byKind);
|
|
153
|
-
if (kinds.length > 0) {
|
|
154
|
-
lines.push("by class:");
|
|
155
|
-
for (const [kind, count] of kinds) {
|
|
156
|
-
lines.push(` ${kind}: ${count}`);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
else {
|
|
160
|
-
lines.push("by class: (none)");
|
|
161
|
-
}
|
|
162
|
-
const sources = Object.entries(summary.bySource);
|
|
163
|
-
if (sources.length > 0) {
|
|
164
|
-
lines.push("by source:");
|
|
165
|
-
for (const [source, count] of sources) {
|
|
166
|
-
lines.push(` ${source}: ${count}`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return lines.join("\n");
|
|
170
|
-
}
|
|
171
|
-
//# sourceMappingURL=failures.js.map
|
package/lib/fate.d.ts
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Gate local-fate dimension (#11 P2): the automatic review gate also decides
|
|
3
|
-
* what happens to a session's local entries while the session is STILL
|
|
4
|
-
* running — not only at wrap-up.
|
|
5
|
-
*
|
|
6
|
-
* Local entries default to orphans: a later session (not on the parentSession
|
|
7
|
-
* chain) never sees them, and nothing promotes or archives them — the
|
|
8
|
-
* exploration results effectively "die" with the session. This module gives
|
|
9
|
-
* the gate its own fate cadence on top of the existing review:
|
|
10
|
-
*
|
|
11
|
-
* - `promote` → the entry moves into the global store. The global store is a
|
|
12
|
-
* governed resource: the user is consulted FIRST (the consultSkillEdits
|
|
13
|
-
* pattern — never written silently), with a cooldown after a decline.
|
|
14
|
-
* - `archive` → hidden from injection (data stays restorable). Archives that
|
|
15
|
-
* would bury possibly-reusable content (not covered globally AND distilled
|
|
16
|
-
* from real user messages) ask the user first; covered/operational entries
|
|
17
|
-
* archive silently, exactly like the wrap-up command does.
|
|
18
|
-
* - `keep` → nothing.
|
|
19
|
-
*
|
|
20
|
-
* At compaction the gate NEVER opens a dialog (the agent is mid-compaction):
|
|
21
|
-
* only deterministic silent archives apply, everything governed is deferred
|
|
22
|
-
* with an audit record pointing at `/evolve wrapup`.
|
|
23
|
-
*
|
|
24
|
-
* Division of labor is the same as wrap-up: the mechanical audit proposes
|
|
25
|
-
* (listLocalCandidates + coverage guards), the LLM classifies
|
|
26
|
-
* (assessLocalEntries), the user approves, the code applies deterministically
|
|
27
|
-
* (wholePromoteProposals / splitPromoteProposals — the SAME edits the wrap-up
|
|
28
|
-
* command applies). Every decision lands in reviews.jsonl via the gate's
|
|
29
|
-
* record callback.
|
|
30
|
-
*/
|
|
31
|
-
import type { Context } from "@deepseek-ai/cordis";
|
|
32
|
-
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
33
|
-
import type { HarnessState, RefinementResult } from "./types.js";
|
|
34
|
-
import type { EvolutionEngine } from "./service.js";
|
|
35
|
-
import type { AutoRefineReason } from "./review.js";
|
|
36
|
-
import type { AutoReviewConfig, GateState, ReviewRecord } from "./auto.js";
|
|
37
|
-
import { type PromotionPolicy } from "./promotion.js";
|
|
38
|
-
import { type RecordLanguage } from "./record-language.js";
|
|
39
|
-
import { type WrapupCandidate, type WrapupItem } from "./wrapup.js";
|
|
40
|
-
/** Turns a declined local-fate proposal stays silent before being offered again. */
|
|
41
|
-
export declare const FATE_CONSULT_COOLDOWN_TURNS = 10;
|
|
42
|
-
/** What the gate decided to do with the session's local entries. */
|
|
43
|
-
export interface FatePlan {
|
|
44
|
-
candidates: readonly WrapupCandidate[];
|
|
45
|
-
/** Whole promotions that passed the deterministic global-coverage guard. */
|
|
46
|
-
promotable: WrapupItem[];
|
|
47
|
-
/** Split promotions (archive + cleaned promote payload) that passed the guard. */
|
|
48
|
-
splits: {
|
|
49
|
-
item: WrapupItem;
|
|
50
|
-
candidate: WrapupCandidate;
|
|
51
|
-
}[];
|
|
52
|
-
/** Archives that may proceed silently (covered globally / no real distillation source). */
|
|
53
|
-
silentArchives: WrapupItem[];
|
|
54
|
-
/** Archives that must ask the user first (uncovered + real source). */
|
|
55
|
-
reviewArchives: WrapupItem[];
|
|
56
|
-
/** Promotes blocked by the deterministic guard, with why. */
|
|
57
|
-
skipped: {
|
|
58
|
-
key: string;
|
|
59
|
-
reason: string;
|
|
60
|
-
}[];
|
|
61
|
-
/** Split promotions blocked by the deterministic guard, with why. */
|
|
62
|
-
splitSkipped: {
|
|
63
|
-
key: string;
|
|
64
|
-
reason: string;
|
|
65
|
-
}[];
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Partition an assessed wrap-up classification into concrete fate actions,
|
|
69
|
-
* re-running the deterministic guards against the LIVE global store (state
|
|
70
|
-
* may have changed while the LLM call was in flight). Pure and unit-tested;
|
|
71
|
-
* mirrors the partition step of the wrap-up command.
|
|
72
|
-
*/
|
|
73
|
-
export declare function planLocalFates(items: readonly WrapupItem[], candidates: readonly WrapupCandidate[], globalState: HarnessState, policy?: PromotionPolicy): FatePlan;
|
|
74
|
-
/**
|
|
75
|
-
* The cooldown key of a candidate set: the sorted `kind:id` list. The set is
|
|
76
|
-
* the unit of consultation — a declined proposal is not offered again within
|
|
77
|
-
* the cooldown window, and a changed set (new entries appeared) starts a
|
|
78
|
-
* fresh consultation.
|
|
79
|
-
*/
|
|
80
|
-
export declare function fateSetKey(candidates: readonly WrapupCandidate[]): string;
|
|
81
|
-
/**
|
|
82
|
-
* Whether the local-fate dimension is due for this gate run. Turn-interval
|
|
83
|
-
* gates respect the fate cadence (an independent counter — goal-driven
|
|
84
|
-
* sessions run the review EVERY round, the fate assessment must not);
|
|
85
|
-
* compaction is unconditional: experiences about to be summarized away get
|
|
86
|
-
* their fate check regardless. Goal-blocked assessments are unconditional
|
|
87
|
-
* here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
|
|
88
|
-
* already gates their frequency, so the cadence must not re-block them.
|
|
89
|
-
*/
|
|
90
|
-
export declare function fateCadenceDue(state: GateState, reason: AutoRefineReason, intervalTurns: number): boolean;
|
|
91
|
-
export interface FateConsultResult {
|
|
92
|
-
approved: boolean;
|
|
93
|
-
asked: boolean;
|
|
94
|
-
reason: "nothing-to-ask" | "consented" | "declined" | "cooldown" | "unavailable" | "error";
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Ask the user whether to execute the gate's local-fate proposal. ONE dialog
|
|
98
|
-
* covers every governed action (promotes, split promotions, review-required
|
|
99
|
-
* archives) — the gate never spams questions. Conservative on every edge:
|
|
100
|
-
* no question service → not approved; the question call fails → not approved;
|
|
101
|
-
* the same candidate set was declined within the cooldown → not asked again.
|
|
102
|
-
* A decline records the cooldown (the consultSkillEdits pattern).
|
|
103
|
-
*/
|
|
104
|
-
export declare function consultLocalFates(ctx: Context, agent: Agent, plan: FatePlan, gate: GateState, language?: RecordLanguage): Promise<FateConsultResult>;
|
|
105
|
-
export type FateApplyMode = "full" | "silent-only";
|
|
106
|
-
export interface FateApplyResult {
|
|
107
|
-
/** Human-readable lines of what was applied (for the notice and the audit record). */
|
|
108
|
-
applied: string[];
|
|
109
|
-
/** Every refinement result produced, for rollback discovery. */
|
|
110
|
-
results: RefinementResult[];
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Deterministically apply the fate plan. `"full"` applies everything
|
|
114
|
-
* (promotes, splits, silent + review archives); `"silent-only"` applies only
|
|
115
|
-
* the deterministic silent archives (the compaction path — nothing governed).
|
|
116
|
-
* Promotes go through the SAME proposals as the wrap-up command
|
|
117
|
-
* (wholePromoteProposals / splitPromoteProposals), so both paths write
|
|
118
|
-
* identical global entries and local retirement stamps.
|
|
119
|
-
*/
|
|
120
|
-
export declare function applyLocalFates(engine: EvolutionEngine, sessionId: string, plan: FatePlan, localState: HarnessState, mode: FateApplyMode): FateApplyResult;
|
|
121
|
-
/**
|
|
122
|
-
* The gate's local-fate phase. Runs after the review phase on every gate
|
|
123
|
-
* trigger (turn_snapshot / compact), subject to cadence and cooldown. All
|
|
124
|
-
* failures are contained and recorded — a broken fate dimension never
|
|
125
|
-
* disturbs the agent loop.
|
|
126
|
-
*/
|
|
127
|
-
export declare function runLocalFatePhase(ctx: Context, engine: EvolutionEngine, agent: Agent, config: AutoReviewConfig, state: GateState, reason: AutoRefineReason, record: (entry: Omit<ReviewRecord, "timestamp">) => void): Promise<void>;
|
|
128
|
-
/** The user-visible notice after the gate applied local-fate actions. */
|
|
129
|
-
export declare function buildFateNotice(applied: readonly string[]): string;
|
|
130
|
-
//# sourceMappingURL=fate.d.ts.map
|
package/lib/fate.js
DELETED
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
|
-
import { EVOLVE_MESSAGE_SOURCE } from "./message-source.js";
|
|
3
|
-
import { DEFAULT_PROMOTION_POLICY } from "./promotion.js";
|
|
4
|
-
import { questionServiceOf } from "./approval.js";
|
|
5
|
-
import { fateConsultCopy, resolveDialogLanguage } from "./copy.js";
|
|
6
|
-
import { resolveRecordLanguage } from "./record-language.js";
|
|
7
|
-
import { assessLocalEntries, candidateKey, filterPromotable, listLocalCandidates, splitArchiveGuards, splitPromoteBlocked, splitPromoteProposals, wholePromoteProposals, } from "./wrapup.js";
|
|
8
|
-
/** Turns a declined local-fate proposal stays silent before being offered again. */
|
|
9
|
-
export const FATE_CONSULT_COOLDOWN_TURNS = 10;
|
|
10
|
-
/**
|
|
11
|
-
* Partition an assessed wrap-up classification into concrete fate actions,
|
|
12
|
-
* re-running the deterministic guards against the LIVE global store (state
|
|
13
|
-
* may have changed while the LLM call was in flight). Pure and unit-tested;
|
|
14
|
-
* mirrors the partition step of the wrap-up command.
|
|
15
|
-
*/
|
|
16
|
-
export function planLocalFates(items, candidates, globalState, policy = DEFAULT_PROMOTION_POLICY) {
|
|
17
|
-
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
18
|
-
const { promotable, skipped } = filterPromotable(items, globalState, candidates, policy);
|
|
19
|
-
const promoteItems = promotable.filter((item) => item.verdict === "promote");
|
|
20
|
-
const archiveItems = items.filter((item) => item.verdict === "archive");
|
|
21
|
-
const splits = [];
|
|
22
|
-
const splitSkipped = [];
|
|
23
|
-
for (const item of archiveItems) {
|
|
24
|
-
if (!item.promote)
|
|
25
|
-
continue;
|
|
26
|
-
const candidate = byKey.get(item.key);
|
|
27
|
-
if (!candidate) {
|
|
28
|
-
splitSkipped.push({ key: item.key, reason: "not in the audited candidate list" });
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
const blocked = splitPromoteBlocked(item, globalState, candidate.kind, policy, candidate.metadata);
|
|
32
|
-
if (blocked) {
|
|
33
|
-
splitSkipped.push({ key: item.key, reason: blocked });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
splits.push({ item, candidate });
|
|
37
|
-
}
|
|
38
|
-
const plainArchives = archiveItems.filter((item) => !item.promote);
|
|
39
|
-
const { silent: silentArchives, review: reviewArchives } = splitArchiveGuards(plainArchives, candidates);
|
|
40
|
-
return { candidates, promotable: promoteItems, splits, silentArchives, reviewArchives, skipped, splitSkipped };
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* The cooldown key of a candidate set: the sorted `kind:id` list. The set is
|
|
44
|
-
* the unit of consultation — a declined proposal is not offered again within
|
|
45
|
-
* the cooldown window, and a changed set (new entries appeared) starts a
|
|
46
|
-
* fresh consultation.
|
|
47
|
-
*/
|
|
48
|
-
export function fateSetKey(candidates) {
|
|
49
|
-
return candidates
|
|
50
|
-
.map((candidate) => candidateKey(candidate.kind, candidate.id))
|
|
51
|
-
.sort()
|
|
52
|
-
.join("|");
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Whether the local-fate dimension is due for this gate run. Turn-interval
|
|
56
|
-
* gates respect the fate cadence (an independent counter — goal-driven
|
|
57
|
-
* sessions run the review EVERY round, the fate assessment must not);
|
|
58
|
-
* compaction is unconditional: experiences about to be summarized away get
|
|
59
|
-
* their fate check regardless. Goal-blocked assessments are unconditional
|
|
60
|
-
* here too — the gate's own streak counter (auto.ts runGoalBlockedFate)
|
|
61
|
-
* already gates their frequency, so the cadence must not re-block them.
|
|
62
|
-
*/
|
|
63
|
-
export function fateCadenceDue(state, reason, intervalTurns) {
|
|
64
|
-
// Compact and goal-blocked assessments bypass the cadence (see header).
|
|
65
|
-
if (reason === "compact" || reason === "goal_blocked")
|
|
66
|
-
return true;
|
|
67
|
-
return state.turns - state.lastFateAt >= intervalTurns;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Ask the user whether to execute the gate's local-fate proposal. ONE dialog
|
|
71
|
-
* covers every governed action (promotes, split promotions, review-required
|
|
72
|
-
* archives) — the gate never spams questions. Conservative on every edge:
|
|
73
|
-
* no question service → not approved; the question call fails → not approved;
|
|
74
|
-
* the same candidate set was declined within the cooldown → not asked again.
|
|
75
|
-
* A decline records the cooldown (the consultSkillEdits pattern).
|
|
76
|
-
*/
|
|
77
|
-
export async function consultLocalFates(ctx, agent, plan, gate, language) {
|
|
78
|
-
const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
|
|
79
|
-
if (!needsDialog) {
|
|
80
|
-
return { approved: true, asked: false, reason: "nothing-to-ask" };
|
|
81
|
-
}
|
|
82
|
-
const setKey = fateSetKey(plan.candidates);
|
|
83
|
-
const lastReject = gate.fateRejects.get(setKey);
|
|
84
|
-
if (lastReject !== undefined && gate.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
|
|
85
|
-
return { approved: false, asked: false, reason: "cooldown" };
|
|
86
|
-
}
|
|
87
|
-
const userQuestions = questionServiceOf(ctx);
|
|
88
|
-
if (!userQuestions) {
|
|
89
|
-
return { approved: false, asked: false, reason: "unavailable" };
|
|
90
|
-
}
|
|
91
|
-
const lang = language ?? resolveDialogLanguage(ctx);
|
|
92
|
-
try {
|
|
93
|
-
const copy = fateConsultCopy(sectionsOf(plan, lang), lang);
|
|
94
|
-
const answer = await userQuestions.ask({
|
|
95
|
-
questions: [
|
|
96
|
-
{
|
|
97
|
-
id: "evolve-fate-consult",
|
|
98
|
-
question: copy.question,
|
|
99
|
-
options: copy.options,
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
agent,
|
|
103
|
-
});
|
|
104
|
-
const selected = answer.answers?.find((entry) => entry.id === "evolve-fate-consult")?.selected ?? [];
|
|
105
|
-
const approved = selected.includes("执行") || selected.includes("Proceed");
|
|
106
|
-
if (!approved) {
|
|
107
|
-
gate.fateRejects.set(setKey, gate.turns);
|
|
108
|
-
}
|
|
109
|
-
return { approved, asked: true, reason: approved ? "consented" : "declined" };
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return { approved: false, asked: true, reason: "error" };
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/** Split a fate plan into pre-rendered per-group lines for the dialog copy. */
|
|
116
|
-
function sectionsOf(plan, lang) {
|
|
117
|
-
const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
118
|
-
const promotable = [];
|
|
119
|
-
const splits = [];
|
|
120
|
-
const reviewArchives = [];
|
|
121
|
-
for (const item of plan.promotable) {
|
|
122
|
-
promotable.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
|
|
123
|
-
}
|
|
124
|
-
for (const { item } of plan.splits) {
|
|
125
|
-
splits.push(lang === "zh"
|
|
126
|
-
? `- ${item.key} → 拆出提升「${item.promote?.title}」(原条目随之归档)`
|
|
127
|
-
: `- ${item.key} → split-promote "${item.promote?.title}" (original archived alongside)`);
|
|
128
|
-
}
|
|
129
|
-
for (const item of plan.reviewArchives) {
|
|
130
|
-
reviewArchives.push(`- ${item.key}「${byKey.get(item.key)?.title ?? item.key}」 — ${item.reason}`);
|
|
131
|
-
}
|
|
132
|
-
return { promotable, splits, reviewArchives };
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Deterministically apply the fate plan. `"full"` applies everything
|
|
136
|
-
* (promotes, splits, silent + review archives); `"silent-only"` applies only
|
|
137
|
-
* the deterministic silent archives (the compaction path — nothing governed).
|
|
138
|
-
* Promotes go through the SAME proposals as the wrap-up command
|
|
139
|
-
* (wholePromoteProposals / splitPromoteProposals), so both paths write
|
|
140
|
-
* identical global entries and local retirement stamps.
|
|
141
|
-
*/
|
|
142
|
-
export function applyLocalFates(engine, sessionId, plan, localState, mode) {
|
|
143
|
-
const applied = [];
|
|
144
|
-
const results = [];
|
|
145
|
-
const byKey = new Map(plan.candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
146
|
-
if (mode === "full") {
|
|
147
|
-
for (const item of plan.promotable) {
|
|
148
|
-
const candidate = byKey.get(item.key);
|
|
149
|
-
if (!candidate)
|
|
150
|
-
continue;
|
|
151
|
-
const proposals = wholePromoteProposals(item, candidate, sessionId);
|
|
152
|
-
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
153
|
-
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
154
|
-
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
155
|
-
scope: "local",
|
|
156
|
-
baselineState: localState,
|
|
157
|
-
});
|
|
158
|
-
results.push(globalResult, localResult);
|
|
159
|
-
applied.push(`提升 ${item.key} → 全局 store(global:${createdId},本地副本已归档)`);
|
|
160
|
-
}
|
|
161
|
-
for (const { item, candidate } of plan.splits) {
|
|
162
|
-
if (!item.promote)
|
|
163
|
-
continue;
|
|
164
|
-
const proposals = splitPromoteProposals(item, candidate, sessionId);
|
|
165
|
-
const globalResult = engine.apply("global", undefined, proposals.global, { scope: "global" });
|
|
166
|
-
const createdId = globalResult.appliedEdits.find((edit) => edit.applied)?.id ?? candidate.id;
|
|
167
|
-
const localResult = engine.apply("local", sessionId, proposals.localStamp(createdId), {
|
|
168
|
-
scope: "local",
|
|
169
|
-
baselineState: localState,
|
|
170
|
-
});
|
|
171
|
-
results.push(globalResult, localResult);
|
|
172
|
-
applied.push(`拆解 ${item.key} → 清洗「${item.promote.title}」入全局(原条目已归档)`);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const archives = mode === "full" ? [...plan.silentArchives, ...plan.reviewArchives] : plan.silentArchives;
|
|
176
|
-
for (const item of archives) {
|
|
177
|
-
const candidate = byKey.get(item.key);
|
|
178
|
-
if (!candidate)
|
|
179
|
-
continue;
|
|
180
|
-
const proposal = {
|
|
181
|
-
summary: `gate fate: archive local ${item.key} — ${item.reason}`,
|
|
182
|
-
rationale: item.reason,
|
|
183
|
-
expectedOutcome: "The entry stops being injected but stays restorable.",
|
|
184
|
-
edits: [{ action: "archive", kind: candidate.kind, id: candidate.id }],
|
|
185
|
-
};
|
|
186
|
-
const result = engine.apply("local", sessionId, proposal, { scope: "local", baselineState: localState });
|
|
187
|
-
results.push(result);
|
|
188
|
-
applied.push(`归档 ${item.key}「${candidate.title}」`);
|
|
189
|
-
}
|
|
190
|
-
return { applied, results };
|
|
191
|
-
}
|
|
192
|
-
/**
|
|
193
|
-
* The gate's local-fate phase. Runs after the review phase on every gate
|
|
194
|
-
* trigger (turn_snapshot / compact), subject to cadence and cooldown. All
|
|
195
|
-
* failures are contained and recorded — a broken fate dimension never
|
|
196
|
-
* disturbs the agent loop.
|
|
197
|
-
*/
|
|
198
|
-
export async function runLocalFatePhase(ctx, engine, agent, config, state, reason, record) {
|
|
199
|
-
if (!config.localFate)
|
|
200
|
-
return;
|
|
201
|
-
const logger = ctx.logger("continual-evolve");
|
|
202
|
-
const sessionId = agent.id;
|
|
203
|
-
const tokenUsage = {
|
|
204
|
-
baseDir: engine.baseDir,
|
|
205
|
-
sessionId,
|
|
206
|
-
retain: engine.retention.tokenUsage,
|
|
207
|
-
onError: (cause) => logger.warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
208
|
-
};
|
|
209
|
-
const localState = engine.load("local", sessionId);
|
|
210
|
-
const globalState = engine.load("global", undefined);
|
|
211
|
-
const candidates = listLocalCandidates(localState, globalState, engine.baseDir);
|
|
212
|
-
if (candidates.length === 0)
|
|
213
|
-
return;
|
|
214
|
-
if (!fateCadenceDue(state, reason, config.fateIntervalTurns))
|
|
215
|
-
return;
|
|
216
|
-
const setKey = fateSetKey(candidates);
|
|
217
|
-
const lastReject = state.fateRejects.get(setKey);
|
|
218
|
-
if (lastReject !== undefined && state.turns - lastReject < FATE_CONSULT_COOLDOWN_TURNS) {
|
|
219
|
-
logger.info(`auto-review local-fate skipped [${sessionId}]: candidate set declined ${state.turns - lastReject} turns ago (cooldown)`);
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
const turnsSinceFate = state.turns - state.lastFateAt;
|
|
223
|
-
state.lastFateAt = state.turns;
|
|
224
|
-
let assessment;
|
|
225
|
-
const fateLanguage = resolveRecordLanguage({ configured: config.recordLanguage, ctx });
|
|
226
|
-
try {
|
|
227
|
-
assessment = await assessLocalEntries(ctx, agent, candidates, { tokenUsage, tokenUsagePhase: "fate", language: fateLanguage });
|
|
228
|
-
}
|
|
229
|
-
catch (cause) {
|
|
230
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
231
|
-
logger.warn(`auto-review local-fate failed for ${sessionId}: ${message}`);
|
|
232
|
-
record({
|
|
233
|
-
sessionId,
|
|
234
|
-
reason,
|
|
235
|
-
turnsSinceLastReview: turnsSinceFate,
|
|
236
|
-
outcome: "failed",
|
|
237
|
-
rationale: `fate assessment error: ${message}`,
|
|
238
|
-
});
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const plan = planLocalFates(assessment.items, candidates, globalState, config.promotionPolicy);
|
|
242
|
-
const needsDialog = plan.promotable.length + plan.splits.length + plan.reviewArchives.length > 0;
|
|
243
|
-
let consent = { approved: false, asked: false, reason: "nothing-to-ask" };
|
|
244
|
-
// Turn-path tightening (2026-09-25): turn_snapshot/turn_interval never open
|
|
245
|
-
// a dialog — governed actions are deferred to `/evolve wrapup`. Only an
|
|
246
|
-
// explicit goal_blocked signal (own streak counter) may consult.
|
|
247
|
-
if (reason === "goal_blocked" && needsDialog) {
|
|
248
|
-
consent = await consultLocalFates(ctx, agent, plan, state, fateLanguage);
|
|
249
|
-
}
|
|
250
|
-
if (consent.approved) {
|
|
251
|
-
const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "full");
|
|
252
|
-
logger.info(`auto-review local-fate approved (${reason}) [${sessionId}]: ${plan.promotable.length} promoted, ${plan.splits.length} split, ${plan.reviewArchives.length} review-archived, ${plan.silentArchives.length} silent-archived — ${assessment.rationale}`);
|
|
253
|
-
record({
|
|
254
|
-
sessionId,
|
|
255
|
-
reason,
|
|
256
|
-
turnsSinceLastReview: turnsSinceFate,
|
|
257
|
-
outcome: "approved",
|
|
258
|
-
rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
|
|
259
|
-
refinementId: results.map((result) => result.id).join(","),
|
|
260
|
-
});
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
|
-
if (consent.reason === "declined" || consent.reason === "unavailable" || consent.reason === "error") {
|
|
264
|
-
const withheld = consent.reason;
|
|
265
|
-
const outcome = withheld === "declined" ? "declined" : "deferred";
|
|
266
|
-
logger.info(`auto-review local-fate ${outcome} (${reason}) [${sessionId}]: ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld — ${assessment.rationale}`);
|
|
267
|
-
record({
|
|
268
|
-
sessionId,
|
|
269
|
-
reason,
|
|
270
|
-
turnsSinceLastReview: turnsSinceFate,
|
|
271
|
-
outcome,
|
|
272
|
-
rationale: `fate ${withheld}: ${assessment.rationale} (${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives withheld)`,
|
|
273
|
-
});
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (needsDialog && reason !== "goal_blocked") {
|
|
277
|
-
// Turn/compact path: no dialog. Only deterministic silent archives apply;
|
|
278
|
-
// governed actions are deferred with an audit record pointing at wrapup.
|
|
279
|
-
const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
|
|
280
|
-
if (applied.length > 0) {
|
|
281
|
-
logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} — ${assessment.rationale}`);
|
|
282
|
-
record({
|
|
283
|
-
sessionId,
|
|
284
|
-
reason,
|
|
285
|
-
turnsSinceLastReview: turnsSinceFate,
|
|
286
|
-
outcome: "approved",
|
|
287
|
-
rationale: `fate (${reason}): ${assessment.rationale} (${applied.join("; ")})`,
|
|
288
|
-
refinementId: results.map((result) => result.id).join(","),
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
record({
|
|
292
|
-
sessionId,
|
|
293
|
-
reason,
|
|
294
|
-
turnsSinceLastReview: turnsSinceFate,
|
|
295
|
-
outcome: "deferred",
|
|
296
|
-
rationale: `fate (${reason}): ${plan.promotable.length} promotes, ${plan.splits.length} splits, ${plan.reviewArchives.length} review-archives deferred — run /evolve wrapup for a full session exit`,
|
|
297
|
-
});
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
// Silent archives on a plain turn-interval gate with nothing to ask.
|
|
301
|
-
if (plan.silentArchives.length > 0) {
|
|
302
|
-
const { applied, results } = applyLocalFates(engine, sessionId, plan, localState, "silent-only");
|
|
303
|
-
logger.info(`auto-review local-fate (${reason}) [${sessionId}]: silent-archived ${applied.length} covered/operational entries — ${assessment.rationale}`);
|
|
304
|
-
record({
|
|
305
|
-
sessionId,
|
|
306
|
-
reason,
|
|
307
|
-
turnsSinceLastReview: turnsSinceFate,
|
|
308
|
-
outcome: "approved",
|
|
309
|
-
rationale: `fate: ${assessment.rationale} (${applied.join("; ")})`,
|
|
310
|
-
refinementId: results.map((result) => result.id).join(","),
|
|
311
|
-
});
|
|
312
|
-
if (config.notifyOnAutoReview && (reason === "turn_snapshot" || reason === "turn_interval")) {
|
|
313
|
-
notifyFateApplied(ctx, agent, applied);
|
|
314
|
-
}
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
// Assessed, nothing to do.
|
|
318
|
-
logger.info(`auto-review local-fate assessed (${reason}) [${sessionId}]: ${candidates.length} candidates, no action — ${assessment.rationale}`);
|
|
319
|
-
record({
|
|
320
|
-
sessionId,
|
|
321
|
-
reason,
|
|
322
|
-
turnsSinceLastReview: turnsSinceFate,
|
|
323
|
-
outcome: "assessed",
|
|
324
|
-
rationale: `fate: ${assessment.rationale} (${candidates.length} candidates, no action)`,
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
/** The user-visible notice after the gate applied local-fate actions. */
|
|
328
|
-
export function buildFateNotice(applied) {
|
|
329
|
-
return [
|
|
330
|
-
"🔎 自动进化门禁:本会话 local 条目归宿处理完成:",
|
|
331
|
-
applied.map((line) => `- ${line}`).join("\n"),
|
|
332
|
-
"查看全部条目:/evolve list;撤销:/evolve rollback <refinement id>",
|
|
333
|
-
"请用一句话简短确认即可,不要调用任何工具。",
|
|
334
|
-
].join("\n");
|
|
335
|
-
}
|
|
336
|
-
/** Queue the follow-up notice turn (successful-turn path only). */
|
|
337
|
-
function notifyFateApplied(ctx, agent, applied) {
|
|
338
|
-
try {
|
|
339
|
-
agent.followup(createUserMessage({
|
|
340
|
-
content: [{ type: "text", text: buildFateNotice(applied) }],
|
|
341
|
-
source: EVOLVE_MESSAGE_SOURCE,
|
|
342
|
-
}));
|
|
343
|
-
}
|
|
344
|
-
catch (cause) {
|
|
345
|
-
ctx
|
|
346
|
-
.logger("continual-evolve")
|
|
347
|
-
.warn(`local-fate notice failed for ${agent.id}: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
//# sourceMappingURL=fate.js.map
|
package/lib/goal-command.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `/evolve goal` subcommand handler. Extracted from command.ts (P2-2).
|
|
3
|
-
*/
|
|
4
|
-
import type { Context } from "@deepseek-ai/cordis";
|
|
5
|
-
import type { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
|
|
6
|
-
export declare function executeGoalCommand(ctx: Context, invocation: CommandInvocation, rest: string[]): CommandResult;
|
|
7
|
-
//# sourceMappingURL=goal-command.d.ts.map
|