dsh-continual-evolve 0.7.0 → 0.7.2
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 +22 -8
- package/README.zh.md +17 -8
- package/lib/apply.js +17 -9
- package/lib/auto.d.ts +24 -5
- package/lib/auto.js +159 -71
- package/lib/command.d.ts +5 -0
- package/lib/command.js +141 -3
- package/lib/evolve-event.d.ts +4 -2
- package/lib/evolve-event.js +12 -2
- package/lib/fate.d.ts +1 -1
- package/lib/fate.js +11 -5
- package/lib/index.d.ts +60 -4
- package/lib/index.js +52 -22
- package/lib/llm-text.d.ts +23 -0
- package/lib/llm-text.js +50 -33
- package/lib/planner.d.ts +17 -0
- package/lib/planner.js +23 -5
- package/lib/prefix-cache.d.ts +78 -0
- package/lib/prefix-cache.js +168 -0
- package/lib/review-scheduler.d.ts +37 -0
- package/lib/review-scheduler.js +110 -0
- package/lib/review.d.ts +14 -1
- package/lib/review.js +23 -2
- package/lib/runtime.d.ts +24 -0
- package/lib/runtime.js +87 -0
- package/lib/service.d.ts +11 -1
- package/lib/service.js +19 -3
- package/lib/store.d.ts +38 -0
- package/lib/store.js +88 -1
- package/lib/token-usage.d.ts +68 -0
- package/lib/token-usage.js +211 -0
- package/lib/tool.d.ts +9 -0
- package/lib/tool.js +37 -6
- package/lib/turn-snapshot.d.ts +58 -0
- package/lib/turn-snapshot.js +188 -0
- package/lib/wrapup-command.js +10 -1
- package/lib/wrapup.d.ts +5 -0
- package/lib/wrapup.js +6 -0
- package/package.json +1 -1
package/lib/tool.d.ts
CHANGED
|
@@ -11,6 +11,15 @@ import type { EvolutionEngine } from "./service.js";
|
|
|
11
11
|
* parameter (`global: true`) as fallback. `scope` wins when both are given.
|
|
12
12
|
*/
|
|
13
13
|
export declare function scopeOf(value: unknown, fallback: HarnessScope): HarnessScope;
|
|
14
|
+
/**
|
|
15
|
+
* Collect the target ids for an evolve_delete call: legacy single `id`
|
|
16
|
+
* plus the batch `ids` array, de-duplicated in first-seen order. Throws
|
|
17
|
+
* loudly when neither carries an id — a silent no-op delete would look
|
|
18
|
+
* like success while deleting nothing.
|
|
19
|
+
*
|
|
20
|
+
* @throws when both `id` and `ids` are absent or empty.
|
|
21
|
+
*/
|
|
22
|
+
export declare function collectDeleteIds(id: unknown, ids: unknown): string[];
|
|
14
23
|
export interface ToolGateOptions {
|
|
15
24
|
requireGlobalApproval: boolean;
|
|
16
25
|
}
|
package/lib/tool.js
CHANGED
|
@@ -7,6 +7,7 @@ import { CONFLICT_WARN_SCORE, buildConflictNotice, mostSimilarEntry } from "./pr
|
|
|
7
7
|
import { entrySourceOf } from "./source.js";
|
|
8
8
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
9
9
|
import { buildEvolveCompleteEvent, emitEvolveComplete } from "./evolve-event.js";
|
|
10
|
+
import { DEFAULT_REVIEWS_RETAIN } from "./store.js";
|
|
10
11
|
const SCOPES = ["local", "project", "global"];
|
|
11
12
|
/**
|
|
12
13
|
* Accept the string form (`scope: "project"`) with the legacy boolean tool
|
|
@@ -25,6 +26,30 @@ export function scopeOf(value, fallback) {
|
|
|
25
26
|
function sessionIdOf(exec) {
|
|
26
27
|
return exec.agent?.id;
|
|
27
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Collect the target ids for an evolve_delete call: legacy single `id`
|
|
31
|
+
* plus the batch `ids` array, de-duplicated in first-seen order. Throws
|
|
32
|
+
* loudly when neither carries an id — a silent no-op delete would look
|
|
33
|
+
* like success while deleting nothing.
|
|
34
|
+
*
|
|
35
|
+
* @throws when both `id` and `ids` are absent or empty.
|
|
36
|
+
*/
|
|
37
|
+
export function collectDeleteIds(id, ids) {
|
|
38
|
+
const collected = [];
|
|
39
|
+
if (typeof id === "string" && id.length > 0)
|
|
40
|
+
collected.push(id);
|
|
41
|
+
if (Array.isArray(ids)) {
|
|
42
|
+
for (const item of ids) {
|
|
43
|
+
if (typeof item === "string" && item.length > 0 && !collected.includes(item)) {
|
|
44
|
+
collected.push(item);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (collected.length === 0) {
|
|
49
|
+
throw new Error("evolve_delete requires id or a non-empty ids array");
|
|
50
|
+
}
|
|
51
|
+
return collected;
|
|
52
|
+
}
|
|
28
53
|
/**
|
|
29
54
|
* The store id a scope reads/writes: session id for local, derived project
|
|
30
55
|
* key for project, undefined for global. Throws for project when the
|
|
@@ -177,10 +202,15 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
177
202
|
}));
|
|
178
203
|
ctx.tools.register(defineTool({
|
|
179
204
|
name: "evolve_delete",
|
|
180
|
-
description: "Delete
|
|
205
|
+
description: "Delete harness entries by id. Pass `id` for one entry or `ids` for several — a batch lands as one refinement behind one approval. Literal ids win: entries whose stored id itself carries a scope-like prefix (e.g. a legacy `local:…` id) are addressed verbatim.",
|
|
181
206
|
parameters: {
|
|
182
207
|
kind: { type: "string", enum: ["prompt", "memory", "skill", "subagent"], required: true },
|
|
183
|
-
id: { type: "string",
|
|
208
|
+
id: { type: "string", description: "Existing entry id (single delete; combine with ids or use ids for a batch)." },
|
|
209
|
+
ids: {
|
|
210
|
+
type: "array",
|
|
211
|
+
items: { type: "string" },
|
|
212
|
+
description: "Existing entry ids for one batch delete (one refinement, one approval).",
|
|
213
|
+
},
|
|
184
214
|
scope: { type: "string", enum: SCOPES, description: "Target store: 'local' (default), 'project', or 'global'. Wins over the legacy global flag." },
|
|
185
215
|
global: { type: "boolean", description: "Set true to edit the cross-session store (requires human approval)." },
|
|
186
216
|
},
|
|
@@ -191,11 +221,12 @@ export function registerEvolveTools(ctx, engine, opts) {
|
|
|
191
221
|
execute: async (args, exec) => {
|
|
192
222
|
const scope = scopeOf(args.scope ?? args.global, "local");
|
|
193
223
|
const storeId = storeIdFor(scope, exec);
|
|
224
|
+
const ids = collectDeleteIds(args.id, args.ids);
|
|
194
225
|
if (needsApproval(scope) && opts.requireGlobalApproval) {
|
|
195
|
-
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${
|
|
226
|
+
await requireGlobalApproval(ctx, exec.agent, exec.signal, `evolve_delete ${args.kind}:${ids.length > 1 ? `${ids.length} entries (${ids.join(", ")})` : ids[0]} → ${storeLabel(scope)}`);
|
|
196
227
|
}
|
|
197
|
-
const
|
|
198
|
-
return textResult(applyEditsText(engine, scope, storeId,
|
|
228
|
+
const edits = ids.map((id) => ({ action: "delete", kind: args.kind, id }));
|
|
229
|
+
return textResult(applyEditsText(engine, scope, storeId, edits, exec.agent, sessionIdOf(exec)));
|
|
199
230
|
},
|
|
200
231
|
}));
|
|
201
232
|
ctx.tools.register(defineTool({
|
|
@@ -235,7 +266,7 @@ function applyEditsText(engine, scope, storeId, edits, agent, eventSessionId) {
|
|
|
235
266
|
// The event keys on the live session (not the project store key).
|
|
236
267
|
const eventSession = eventSessionId ?? storeId;
|
|
237
268
|
if (applied.length > 0 && eventSession) {
|
|
238
|
-
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", eventSession));
|
|
269
|
+
emitEvolveComplete(engine.baseDir, buildEvolveCompleteEvent(result, "manual_tool", eventSession), engine.retention?.reviews ?? DEFAULT_REVIEWS_RETAIN);
|
|
239
270
|
}
|
|
240
271
|
const lines = [`refinement ${result.id}: ${applied.length} applied, ${failed.length} failed`];
|
|
241
272
|
for (const e of applied) {
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn-boundary snapshots for the automatic review gate.
|
|
3
|
+
*
|
|
4
|
+
* The scheduler must see a bounded, mechanically filtered slice of the
|
|
5
|
+
* current session, not a fresh full-history LLM request on every turn. This
|
|
6
|
+
* module owns the snapshot boundary, cursor-compatible incremental rows, and
|
|
7
|
+
* the cheap eligibility checks that run before any model call.
|
|
8
|
+
*/
|
|
9
|
+
import type { Context } from "@deepseek-ai/cordis";
|
|
10
|
+
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
11
|
+
/** Why a scheduler snapshot was captured. */
|
|
12
|
+
export type TurnSnapshotReason = "turn_snapshot" | "compact";
|
|
13
|
+
/** Mechanical reasons a snapshot can be skipped without an LLM call. */
|
|
14
|
+
export type SnapshotSkipReason = "no-new-events" | "internal-agent" | "direct-memory-write" | "no-user-prose";
|
|
15
|
+
/** One immutable scheduler input. The cursor is a durable event boundary. */
|
|
16
|
+
export interface TurnSnapshot {
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly projectKey?: string;
|
|
19
|
+
readonly turn: number;
|
|
20
|
+
readonly reason: TurnSnapshotReason;
|
|
21
|
+
readonly cursor: string;
|
|
22
|
+
readonly events: readonly unknown[];
|
|
23
|
+
readonly trajectory: string;
|
|
24
|
+
readonly userText: string;
|
|
25
|
+
readonly sourceSeqs: readonly number[];
|
|
26
|
+
readonly eligible: boolean;
|
|
27
|
+
readonly skipReason?: SnapshotSkipReason;
|
|
28
|
+
}
|
|
29
|
+
/** Eligibility result kept separate so tests and future extractors share it. */
|
|
30
|
+
export interface SnapshotEligibility {
|
|
31
|
+
readonly eligible: boolean;
|
|
32
|
+
readonly reason?: SnapshotSkipReason;
|
|
33
|
+
readonly userText: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Capture the current durable surface and select only rows after `cursor`.
|
|
37
|
+
* A seq-bearing session uses `seq:<n>`; older/test surfaces fall back to an
|
|
38
|
+
* event-count cursor so the scheduler still has a deterministic boundary.
|
|
39
|
+
*/
|
|
40
|
+
export declare function captureTurnSnapshot(ctx: Context, agent: Agent, opts: {
|
|
41
|
+
turn: number;
|
|
42
|
+
reason: TurnSnapshotReason;
|
|
43
|
+
cursor?: string;
|
|
44
|
+
maxChars: number;
|
|
45
|
+
}): Promise<TurnSnapshot>;
|
|
46
|
+
/**
|
|
47
|
+
* Decide whether the incremental rows deserve a model call. This intentionally
|
|
48
|
+
* does not decide whether a fact is valuable; that remains the review model's
|
|
49
|
+
* job. It only removes mechanical noise before the scheduler spends tokens.
|
|
50
|
+
*/
|
|
51
|
+
export declare function evaluateSnapshotEligibility(events: readonly unknown[], internalAgent?: boolean): SnapshotEligibility;
|
|
52
|
+
/** True when an assistant row records an explicit evolve memory mutation. */
|
|
53
|
+
export declare function containsDirectMemoryWrite(events: readonly unknown[]): boolean;
|
|
54
|
+
/** Serialize only user/assistant text, retaining the bounded tail. */
|
|
55
|
+
export declare function serializeSnapshotEvents(events: readonly unknown[], maxChars: number): string;
|
|
56
|
+
/** A DSH subagent header is an internal source when its origin says so. */
|
|
57
|
+
export declare function isInternalAgent(agent: Agent | undefined): boolean;
|
|
58
|
+
//# sourceMappingURL=turn-snapshot.d.ts.map
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { sessionEventsOf } from "./inject.js";
|
|
2
|
+
import { projectKeyOf } from "./project.js";
|
|
3
|
+
/**
|
|
4
|
+
* Capture the current durable surface and select only rows after `cursor`.
|
|
5
|
+
* A seq-bearing session uses `seq:<n>`; older/test surfaces fall back to an
|
|
6
|
+
* event-count cursor so the scheduler still has a deterministic boundary.
|
|
7
|
+
*/
|
|
8
|
+
export async function captureTurnSnapshot(ctx, agent, opts) {
|
|
9
|
+
const allEvents = await readSurface(ctx, agent);
|
|
10
|
+
const cursor = boundaryCursor(allEvents);
|
|
11
|
+
const events = eventsAfterCursor(allEvents, opts.cursor);
|
|
12
|
+
const eligibility = evaluateSnapshotEligibility(events, isInternalAgent(agent));
|
|
13
|
+
const projectKey = projectKeyOf(agent);
|
|
14
|
+
return {
|
|
15
|
+
sessionId: agent.id,
|
|
16
|
+
...(projectKey ? { projectKey } : {}),
|
|
17
|
+
turn: opts.turn,
|
|
18
|
+
reason: opts.reason,
|
|
19
|
+
cursor,
|
|
20
|
+
events,
|
|
21
|
+
trajectory: eligibility.eligible ? serializeSnapshotEvents(events, opts.maxChars) : "",
|
|
22
|
+
userText: eligibility.userText,
|
|
23
|
+
sourceSeqs: directUserSeqs(events),
|
|
24
|
+
eligible: eligibility.eligible,
|
|
25
|
+
...(eligibility.reason ? { skipReason: eligibility.reason } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decide whether the incremental rows deserve a model call. This intentionally
|
|
30
|
+
* does not decide whether a fact is valuable; that remains the review model's
|
|
31
|
+
* job. It only removes mechanical noise before the scheduler spends tokens.
|
|
32
|
+
*/
|
|
33
|
+
export function evaluateSnapshotEligibility(events, internalAgent = false) {
|
|
34
|
+
if (internalAgent)
|
|
35
|
+
return { eligible: false, reason: "internal-agent", userText: "" };
|
|
36
|
+
if (events.length === 0)
|
|
37
|
+
return { eligible: false, reason: "no-new-events", userText: "" };
|
|
38
|
+
if (containsDirectMemoryWrite(events))
|
|
39
|
+
return { eligible: false, reason: "direct-memory-write", userText: "" };
|
|
40
|
+
const userText = directUserText(events);
|
|
41
|
+
if (userText.length < 3)
|
|
42
|
+
return { eligible: false, reason: "no-user-prose", userText };
|
|
43
|
+
return { eligible: true, userText };
|
|
44
|
+
}
|
|
45
|
+
/** True when an assistant row records an explicit evolve memory mutation. */
|
|
46
|
+
export function containsDirectMemoryWrite(events) {
|
|
47
|
+
for (const event of events) {
|
|
48
|
+
if (!event || typeof event !== "object")
|
|
49
|
+
continue;
|
|
50
|
+
const row = event;
|
|
51
|
+
if (row.type === "tool/call" || row.type === "tool-call") {
|
|
52
|
+
if (isMemoryMutationName(readNestedString(row.data, ["name", "tool", "toolName"])))
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (row.type === "assistant/message" && containsMemoryMutationName(row.data))
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
/** Serialize only user/assistant text, retaining the bounded tail. */
|
|
61
|
+
export function serializeSnapshotEvents(events, maxChars) {
|
|
62
|
+
const lines = [];
|
|
63
|
+
for (const raw of events) {
|
|
64
|
+
if (!raw || typeof raw !== "object")
|
|
65
|
+
continue;
|
|
66
|
+
const event = raw;
|
|
67
|
+
if (event.type === "user/message" && !isDirectUserEvent(raw))
|
|
68
|
+
continue;
|
|
69
|
+
const role = event.type === "user/message" ? "user" : event.type === "assistant/message" ? "assistant" : null;
|
|
70
|
+
if (role === null)
|
|
71
|
+
continue;
|
|
72
|
+
const text = contentText(event.data?.content).trim();
|
|
73
|
+
if (text.length > 0)
|
|
74
|
+
lines.push(`${role}: ${text}`);
|
|
75
|
+
}
|
|
76
|
+
const joined = lines.join("\n");
|
|
77
|
+
return joined.length <= maxChars ? joined : joined.slice(-maxChars);
|
|
78
|
+
}
|
|
79
|
+
/** A DSH subagent header is an internal source when its origin says so. */
|
|
80
|
+
export function isInternalAgent(agent) {
|
|
81
|
+
const header = agent?.session?.header;
|
|
82
|
+
return header?.origin === "subagent" || header?.origin === "internal" || header?.isSubagent === true;
|
|
83
|
+
}
|
|
84
|
+
async function readSurface(ctx, agent) {
|
|
85
|
+
const reader = ctx.sessionQuery;
|
|
86
|
+
if (reader) {
|
|
87
|
+
const snapshot = await reader.readSurface(agent.id);
|
|
88
|
+
return Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
89
|
+
}
|
|
90
|
+
return sessionEventsOf(agent);
|
|
91
|
+
}
|
|
92
|
+
function boundaryCursor(events) {
|
|
93
|
+
const seqs = events.map(eventSeq).filter((seq) => seq !== undefined);
|
|
94
|
+
return seqs.length > 0 ? `seq:${Math.max(...seqs)}` : `index:${events.length}`;
|
|
95
|
+
}
|
|
96
|
+
function eventsAfterCursor(events, cursor) {
|
|
97
|
+
if (cursor === undefined)
|
|
98
|
+
return events;
|
|
99
|
+
if (cursor.startsWith("seq:")) {
|
|
100
|
+
const boundary = Number(cursor.slice(4));
|
|
101
|
+
if (!Number.isFinite(boundary))
|
|
102
|
+
return events;
|
|
103
|
+
return events.filter((event) => {
|
|
104
|
+
const seq = eventSeq(event);
|
|
105
|
+
return seq === undefined || seq > boundary;
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
if (cursor.startsWith("index:")) {
|
|
109
|
+
const boundary = Number(cursor.slice(6));
|
|
110
|
+
return Number.isInteger(boundary) && boundary >= 0 ? events.slice(boundary) : events;
|
|
111
|
+
}
|
|
112
|
+
return events;
|
|
113
|
+
}
|
|
114
|
+
function directUserText(events) {
|
|
115
|
+
return events
|
|
116
|
+
.filter(isDirectUserEvent)
|
|
117
|
+
.map((event) => contentText(event.data?.content).trim())
|
|
118
|
+
.filter(Boolean)
|
|
119
|
+
.join("\n");
|
|
120
|
+
}
|
|
121
|
+
function directUserSeqs(events) {
|
|
122
|
+
return events
|
|
123
|
+
.filter(isDirectUserEvent)
|
|
124
|
+
.map(eventSeq)
|
|
125
|
+
.filter((seq) => seq !== undefined);
|
|
126
|
+
}
|
|
127
|
+
function isDirectUserEvent(event) {
|
|
128
|
+
if (!event || typeof event !== "object")
|
|
129
|
+
return false;
|
|
130
|
+
const row = event;
|
|
131
|
+
if (row.type !== "user/message")
|
|
132
|
+
return false;
|
|
133
|
+
if (row.data?.visibility === "model-only")
|
|
134
|
+
return false;
|
|
135
|
+
if (row.data?.source?.synthetic === true)
|
|
136
|
+
return false;
|
|
137
|
+
if (row.data?.source?.kind !== undefined && row.data.source.kind !== "user")
|
|
138
|
+
return false;
|
|
139
|
+
return contentText(row.data?.content).trim().length > 0;
|
|
140
|
+
}
|
|
141
|
+
function contentText(content) {
|
|
142
|
+
if (typeof content === "string")
|
|
143
|
+
return content;
|
|
144
|
+
if (!Array.isArray(content))
|
|
145
|
+
return "";
|
|
146
|
+
return content
|
|
147
|
+
.map((block) => {
|
|
148
|
+
if (typeof block === "string")
|
|
149
|
+
return block;
|
|
150
|
+
if (!block || typeof block !== "object")
|
|
151
|
+
return "";
|
|
152
|
+
const value = block;
|
|
153
|
+
if (value.synthetic === true || value.ignored === true || (value.type !== undefined && value.type !== "text"))
|
|
154
|
+
return "";
|
|
155
|
+
return typeof value.text === "string" ? value.text : "";
|
|
156
|
+
})
|
|
157
|
+
.filter(Boolean)
|
|
158
|
+
.join(" ");
|
|
159
|
+
}
|
|
160
|
+
function eventSeq(event) {
|
|
161
|
+
if (!event || typeof event !== "object")
|
|
162
|
+
return undefined;
|
|
163
|
+
const seq = event.seq;
|
|
164
|
+
return typeof seq === "number" && Number.isInteger(seq) ? seq : undefined;
|
|
165
|
+
}
|
|
166
|
+
function containsMemoryMutationName(value) {
|
|
167
|
+
if (typeof value === "string")
|
|
168
|
+
return isMemoryMutationName(value);
|
|
169
|
+
if (Array.isArray(value))
|
|
170
|
+
return value.some(containsMemoryMutationName);
|
|
171
|
+
if (!value || typeof value !== "object")
|
|
172
|
+
return false;
|
|
173
|
+
return Object.values(value).some(containsMemoryMutationName);
|
|
174
|
+
}
|
|
175
|
+
function isMemoryMutationName(value) {
|
|
176
|
+
return value === "evolve_add" || value === "evolve_update" || value === "evolve_delete";
|
|
177
|
+
}
|
|
178
|
+
function readNestedString(value, keys) {
|
|
179
|
+
if (!value || typeof value !== "object")
|
|
180
|
+
return undefined;
|
|
181
|
+
const row = value;
|
|
182
|
+
for (const key of keys) {
|
|
183
|
+
if (typeof row[key] === "string")
|
|
184
|
+
return row[key];
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=turn-snapshot.js.map
|
package/lib/wrapup-command.js
CHANGED
|
@@ -13,7 +13,16 @@ export async function executeWrapupCommand(ctx, engine, invocation, policy = DEF
|
|
|
13
13
|
return success(`(nothing to wrap up: ${sessionId}'s local store has no active, un-promoted entries — use /evolve list to inspect it)`);
|
|
14
14
|
}
|
|
15
15
|
// 1. Classify: the model judges each audited candidate's fate.
|
|
16
|
-
const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
|
|
16
|
+
const assessment = await assessLocalEntries(ctx, invocation.agent, candidates, {
|
|
17
|
+
signal: invocation.signal,
|
|
18
|
+
tokenUsage: {
|
|
19
|
+
baseDir: engine.baseDir,
|
|
20
|
+
sessionId,
|
|
21
|
+
retain: engine.retention.tokenUsage,
|
|
22
|
+
onError: (cause) => ctx.logger("continual-evolve").warn(`token-usage ledger failed for ${sessionId}: ${cause instanceof Error ? cause.message : String(cause)}`),
|
|
23
|
+
},
|
|
24
|
+
tokenUsagePhase: "wrapup",
|
|
25
|
+
});
|
|
17
26
|
const byKey = new Map(candidates.map((candidate) => [candidateKey(candidate.kind, candidate.id), candidate]));
|
|
18
27
|
// 2. Partition by action. Deterministic guards re-check the LIVE global
|
|
19
28
|
// store right before anything lands (state may have changed mid-call).
|
package/lib/wrapup.d.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import type { Context } from "@deepseek-ai/cordis";
|
|
22
22
|
import type { Agent } from "@deepseek-ai/dsh-agent";
|
|
23
23
|
import type { HarnessEntry, HarnessState, RefinementKind, RefinementProposal } from "./types.js";
|
|
24
|
+
import { type TokenUsageTarget } from "./token-usage.js";
|
|
24
25
|
import { type PromotionPolicy } from "./promotion.js";
|
|
25
26
|
/** What should happen to one local entry at session end. */
|
|
26
27
|
export type WrapupVerdict = "promote" | "archive" | "keep";
|
|
@@ -238,6 +239,10 @@ export interface AssessOptions {
|
|
|
238
239
|
maxOutputTokens?: number;
|
|
239
240
|
/** Abort signal forwarded to the model call. */
|
|
240
241
|
signal?: AbortSignal;
|
|
242
|
+
/** Direct-call token ledger destination; phase is selected by the caller. */
|
|
243
|
+
tokenUsage?: TokenUsageTarget;
|
|
244
|
+
/** Ledger phase for this classifier call (manual wrapup or automatic fate). */
|
|
245
|
+
tokenUsagePhase?: "wrapup" | "fate";
|
|
241
246
|
}
|
|
242
247
|
/**
|
|
243
248
|
* Ask the model to classify the audited local candidates. Routes through the
|
package/lib/wrapup.js
CHANGED
|
@@ -4,6 +4,7 @@ import { extractJsonObject } from "./plan.js";
|
|
|
4
4
|
import { compactText } from "./render.js";
|
|
5
5
|
import { streamText } from "./llm-text.js";
|
|
6
6
|
import { getUsageCount, loadUsage } from "./usage.js";
|
|
7
|
+
import { createTokenUsageObserver } from "./token-usage.js";
|
|
7
8
|
import { recencyScore } from "./inject.js";
|
|
8
9
|
import { DEFAULT_PROMOTION_POLICY, mostSimilarGlobalEntry, projectScopedReason, secretLeakReason } from "./promotion.js";
|
|
9
10
|
export function candidateKey(kind, id) {
|
|
@@ -525,6 +526,11 @@ export async function assessLocalEntries(ctx, agent, candidates, options = {}) {
|
|
|
525
526
|
prompt: userPrompt,
|
|
526
527
|
maxTokens: options.maxOutputTokens ?? 4096,
|
|
527
528
|
signal: options.signal,
|
|
529
|
+
...(options.tokenUsage
|
|
530
|
+
? {
|
|
531
|
+
onUsage: createTokenUsageObserver(options.tokenUsage, options.tokenUsagePhase ?? "wrapup", agent.options.provider, agent.options.model),
|
|
532
|
+
}
|
|
533
|
+
: {}),
|
|
528
534
|
});
|
|
529
535
|
return parseWrapupAssessment(text, candidates);
|
|
530
536
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-continual-evolve",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Continual self-evolution plugin for DeepSeek Harness: versioned, auditable, rollback-safe harness state (prompt notes, memories, skills, subagent specs) refined from session trajectories.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|