dsh-harbor-evolution 0.7.2 → 0.8.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 +16 -4
- package/index.js +72 -6
- package/lib/candidate.js +58 -5
- package/lib/client.js +283 -63
- package/lib/dashboard.js +307 -34
- package/lib/evolution.js +328 -20
- package/lib/model-runtime.js +53 -8
- package/lib/runtime-identity.js +7 -0
- package/lib/service.js +187 -33
- package/lib/session-diagnostic.js +320 -0
- package/lib/session-materializer.js +194 -0
- package/lib/session-projection.js +161 -0
- package/lib/session-redaction.js +311 -0
- package/lib/session-selection.js +294 -0
- package/lib/setup.js +11 -5
- package/lib/version.js +128 -0
- package/lib/web.js +5 -1
- package/package.json +13 -3
- package/schemas/dsh-session-observation.schema.json +69 -0
- package/schemas/evaluation-result-v2.schema.json +45 -0
- package/schemas/historical-evaluation-context.schema.json +66 -0
- package/schemas/historical-evaluation-summary.schema.json +49 -0
- package/schemas/historical-generation-batch.schema.json +76 -0
- package/skills/evolve-agent-with-harbor/SKILL.md +127 -13
- package/skills/evolve-agent-with-harbor/evals/evals.json +57 -9
- package/skills/evolve-agent-with-harbor/references/evaluator-upgrade.md +34 -1
- package/skills/evolve-agent-with-harbor/references/initialization.md +9 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { foldSessionDiagnosticIndex } from './session-projection.js'
|
|
5
|
+
|
|
6
|
+
function canonicalize(value) {
|
|
7
|
+
if (Array.isArray(value)) return value.map(canonicalize)
|
|
8
|
+
if (value && typeof value === 'object') {
|
|
9
|
+
return Object.fromEntries(
|
|
10
|
+
Object.keys(value).sort().map(key => [key, canonicalize(value[key])]),
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function canonicalDigest(value, namespace) {
|
|
17
|
+
const body = JSON.stringify(canonicalize(value))
|
|
18
|
+
return `sha256:${createHash('sha256').update(namespace).update('\0').update(body).digest('hex')}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sessionHeaderIdentity(header, effectiveAgentPreset) {
|
|
22
|
+
return {
|
|
23
|
+
version: header?.version,
|
|
24
|
+
id: header?.id,
|
|
25
|
+
createdAt: header?.createdAt,
|
|
26
|
+
cwd: header?.cwd,
|
|
27
|
+
parentSession: header?.parentSession,
|
|
28
|
+
seedLength: header?.seedLength,
|
|
29
|
+
origin: header?.origin,
|
|
30
|
+
delegationDepth: header?.delegationDepth,
|
|
31
|
+
...(effectiveAgentPreset === undefined ? {} : { agentPreset: effectiveAgentPreset }),
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sameProjectRoot(value, projectRoot) {
|
|
36
|
+
if (typeof value !== 'string' || !path.isAbsolute(value)) return false
|
|
37
|
+
return path.resolve(value) === path.resolve(projectRoot)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isoTime(value) {
|
|
41
|
+
return Number.isSafeInteger(value) && value > 0 ? new Date(value).toISOString() : null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeIdentity(value, rawSessionId) {
|
|
45
|
+
const text = typeof value === 'string' ? value : ''
|
|
46
|
+
if (rawSessionId && text.includes(rawSessionId)) return '[redacted-identity]'
|
|
47
|
+
if (
|
|
48
|
+
/(?:api[_-]?key|token|secret|password|authorization|bearer\s+)/i.test(text)
|
|
49
|
+
|| /(?:^|[\\/])(?:Users|home|private|tmp|var|etc|opt|Volumes)(?:[\\/]|$)/.test(text)
|
|
50
|
+
|| /\b(?:sk|rk|pk)-[A-Za-z0-9_-]{12,}\b/.test(text)
|
|
51
|
+
) return '[redacted-identity]'
|
|
52
|
+
return text.slice(0, 160)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function mapConcurrent(values, concurrency, mapper) {
|
|
56
|
+
const result = new Array(values.length)
|
|
57
|
+
let cursor = 0
|
|
58
|
+
const workers = Array.from({ length: Math.min(concurrency, values.length) }, async () => {
|
|
59
|
+
while (cursor < values.length) {
|
|
60
|
+
const index = cursor
|
|
61
|
+
cursor += 1
|
|
62
|
+
try {
|
|
63
|
+
result[index] = { status: 'fulfilled', value: await mapper(values[index], index) }
|
|
64
|
+
} catch (reason) {
|
|
65
|
+
result[index] = { status: 'rejected', reason }
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
await Promise.all(workers)
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function publicSelection(item, index) {
|
|
74
|
+
const agentPreset = item.index.effectiveAgentPreset ?? item.header.agentPreset
|
|
75
|
+
return {
|
|
76
|
+
trialId: item.trialId,
|
|
77
|
+
title: `历史会话 ${index + 1}`,
|
|
78
|
+
createdAt: isoTime(item.header.createdAt),
|
|
79
|
+
lastActivityAt: isoTime(item.index.lastActivityAt),
|
|
80
|
+
turnCount: item.index.turnCount,
|
|
81
|
+
humanMessageCount: item.index.humanMessageCount,
|
|
82
|
+
assistantMessageCount: item.index.assistantMessageCount,
|
|
83
|
+
toolCallCount: item.index.toolCallCount,
|
|
84
|
+
lastTurnReason: item.index.lastTurnReason,
|
|
85
|
+
agentPreset: agentPreset ? safeIdentity(agentPreset, item.rawSessionId) : null,
|
|
86
|
+
modelRoutes: item.index.modelRoutes.map(route => ({
|
|
87
|
+
provider: safeIdentity(route.provider, item.rawSessionId),
|
|
88
|
+
model: safeIdentity(route.model, item.rawSessionId),
|
|
89
|
+
...(route.reasoning_effort ? { reasoning_effort: safeIdentity(route.reasoning_effort, item.rawSessionId) } : {}),
|
|
90
|
+
})),
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function selectRecentSessions({
|
|
95
|
+
sessionQuery,
|
|
96
|
+
projectRoot,
|
|
97
|
+
currentSessionId,
|
|
98
|
+
limit = 10,
|
|
99
|
+
maxSessionReads = 100,
|
|
100
|
+
concurrency = 4,
|
|
101
|
+
createdAfter,
|
|
102
|
+
signal,
|
|
103
|
+
}) {
|
|
104
|
+
if (!sessionQuery || typeof sessionQuery.readSession !== 'function') {
|
|
105
|
+
throw new Error('DSH_SESSION_QUERY_UNAVAILABLE: this DSH Profile does not expose the Session Query service')
|
|
106
|
+
}
|
|
107
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 10) {
|
|
108
|
+
throw new Error('SESSION_LIMIT_INVALID: limit must be an integer from 1 to 10')
|
|
109
|
+
}
|
|
110
|
+
if (createdAfter !== undefined && (!Number.isSafeInteger(createdAfter) || createdAfter < 0)) {
|
|
111
|
+
throw new Error('SESSION_CREATED_AFTER_INVALID: createdAfter must be a valid timestamp')
|
|
112
|
+
}
|
|
113
|
+
const listed = typeof sessionQuery.filterSessions === 'function'
|
|
114
|
+
? await sessionQuery.filterSessions([
|
|
115
|
+
{ kind: 'cwd', values: [projectRoot] },
|
|
116
|
+
...(createdAfter === undefined ? [] : [{ kind: 'created-at', from: createdAfter }]),
|
|
117
|
+
], signal)
|
|
118
|
+
: await sessionQuery.listSessions(signal)
|
|
119
|
+
const excludedCounts = {
|
|
120
|
+
outsideWorkspace: 0,
|
|
121
|
+
beforeCreatedAfter: 0,
|
|
122
|
+
currentSession: 0,
|
|
123
|
+
subagent: 0,
|
|
124
|
+
forkOrChild: 0,
|
|
125
|
+
openTurn: 0,
|
|
126
|
+
noDirectHumanInput: 0,
|
|
127
|
+
noAssistantOutput: 0,
|
|
128
|
+
userAborted: 0,
|
|
129
|
+
harborInternal: 0,
|
|
130
|
+
empty: 0,
|
|
131
|
+
unreadable: 0,
|
|
132
|
+
}
|
|
133
|
+
const candidates = []
|
|
134
|
+
for (const record of Array.isArray(listed) ? listed : []) {
|
|
135
|
+
const header = record?.header ?? {}
|
|
136
|
+
if (!sameProjectRoot(header.cwd, projectRoot)) {
|
|
137
|
+
excludedCounts.outsideWorkspace += 1
|
|
138
|
+
} else if (header.id === currentSessionId) {
|
|
139
|
+
excludedCounts.currentSession += 1
|
|
140
|
+
} else if (createdAfter !== undefined && Number(header.createdAt) < createdAfter) {
|
|
141
|
+
excludedCounts.beforeCreatedAfter += 1
|
|
142
|
+
} else if (header.origin === 'subagent') {
|
|
143
|
+
excludedCounts.subagent += 1
|
|
144
|
+
} else if (
|
|
145
|
+
header.parentSession !== undefined
|
|
146
|
+
|| Number(header.seedLength ?? 0) > 0
|
|
147
|
+
|| Number(header.delegationDepth ?? 0) > 0
|
|
148
|
+
) {
|
|
149
|
+
excludedCounts.forkOrChild += 1
|
|
150
|
+
} else {
|
|
151
|
+
candidates.push(record)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (candidates.length > maxSessionReads) {
|
|
155
|
+
throw new Error(
|
|
156
|
+
`SESSION_SELECTION_TOO_EXPENSIVE: ${candidates.length} exact Session reads exceed maxSessionReads=${maxSessionReads}; preview again with createdAfter to narrow the scan`,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const snapshots = await mapConcurrent(candidates, concurrency, async record => {
|
|
161
|
+
const snapshot = await sessionQuery.readSession(record.header.id)
|
|
162
|
+
if (snapshot?.session?.id !== record.header.id) {
|
|
163
|
+
throw new Error('Session Query returned a mismatched Session header')
|
|
164
|
+
}
|
|
165
|
+
if (!sameProjectRoot(snapshot.session.cwd, projectRoot)) {
|
|
166
|
+
throw new Error('Session Query changed the Session workspace boundary')
|
|
167
|
+
}
|
|
168
|
+
return snapshot
|
|
169
|
+
})
|
|
170
|
+
const eligible = []
|
|
171
|
+
for (const outcome of snapshots) {
|
|
172
|
+
if (outcome.status === 'rejected') {
|
|
173
|
+
excludedCounts.unreadable += 1
|
|
174
|
+
continue
|
|
175
|
+
}
|
|
176
|
+
const snapshot = outcome.value
|
|
177
|
+
const index = foldSessionDiagnosticIndex(snapshot.events, snapshot.session)
|
|
178
|
+
if (index.lastSeq === null) {
|
|
179
|
+
excludedCounts.empty += 1
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
if (index.openTurn) {
|
|
183
|
+
excludedCounts.openTurn += 1
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
if (index.lastTurnReason === 'aborted') {
|
|
187
|
+
excludedCounts.userAborted += 1
|
|
188
|
+
continue
|
|
189
|
+
}
|
|
190
|
+
if (index.humanMessageCount < 1) {
|
|
191
|
+
excludedCounts.noDirectHumanInput += 1
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
if (index.assistantMessageCount < 1) {
|
|
195
|
+
excludedCounts.noAssistantOutput += 1
|
|
196
|
+
continue
|
|
197
|
+
}
|
|
198
|
+
if (index.hasHarborToolCall) {
|
|
199
|
+
excludedCounts.harborInternal += 1
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
if (/harbor/i.test(String(index.effectiveAgentPreset ?? ''))) {
|
|
203
|
+
excludedCounts.harborInternal += 1
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
const header = sessionHeaderIdentity(snapshot.session, index.effectiveAgentPreset)
|
|
207
|
+
const sourceDigest = canonicalDigest(
|
|
208
|
+
{ session: header, events: snapshot.events },
|
|
209
|
+
'harbor-dsh-session-source-v1',
|
|
210
|
+
)
|
|
211
|
+
const sourceRef = canonicalDigest(
|
|
212
|
+
{ id: snapshot.session.id, header },
|
|
213
|
+
'harbor-dsh-session-source-ref-v1',
|
|
214
|
+
)
|
|
215
|
+
eligible.push({
|
|
216
|
+
rawSessionId: snapshot.session.id,
|
|
217
|
+
header,
|
|
218
|
+
events: snapshot.events,
|
|
219
|
+
index,
|
|
220
|
+
sourceDigest,
|
|
221
|
+
sourceRef,
|
|
222
|
+
capturedThroughSeq: index.lastSeq,
|
|
223
|
+
trialId: `session-${sourceRef.slice('sha256:'.length, 'sha256:'.length + 12)}`,
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
eligible.sort((left, right) => (
|
|
227
|
+
right.index.lastActivityAt - left.index.lastActivityAt
|
|
228
|
+
|| left.sourceRef.localeCompare(right.sourceRef)
|
|
229
|
+
))
|
|
230
|
+
const selected = eligible.slice(0, limit)
|
|
231
|
+
return {
|
|
232
|
+
selected,
|
|
233
|
+
publicSelected: selected.map(publicSelection),
|
|
234
|
+
excludedCounts,
|
|
235
|
+
warnings: excludedCounts.unreadable
|
|
236
|
+
? [`${excludedCounts.unreadable} Session(s) could not be read and were excluded.`]
|
|
237
|
+
: [],
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function verifySessionSnapshot(expected, snapshot, projectRoot) {
|
|
242
|
+
if (snapshot?.session?.id !== expected.rawSessionId || !sameProjectRoot(snapshot?.session?.cwd, projectRoot)) {
|
|
243
|
+
return false
|
|
244
|
+
}
|
|
245
|
+
const index = foldSessionDiagnosticIndex(snapshot.events, snapshot.session)
|
|
246
|
+
if (index.lastSeq !== expected.capturedThroughSeq || index.openTurn) return false
|
|
247
|
+
const digest = canonicalDigest(
|
|
248
|
+
{ session: sessionHeaderIdentity(snapshot.session, index.effectiveAgentPreset), events: snapshot.events },
|
|
249
|
+
'harbor-dsh-session-source-v1',
|
|
250
|
+
)
|
|
251
|
+
return digest === expected.sourceDigest
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export class SessionSelectionTokenStore {
|
|
255
|
+
constructor({ ttlMs = 15 * 60 * 1000, now = () => Date.now(), randomToken } = {}) {
|
|
256
|
+
this.ttlMs = ttlMs
|
|
257
|
+
this.now = now
|
|
258
|
+
this.randomToken = randomToken ?? (() => randomBytes(32).toString('base64url'))
|
|
259
|
+
this.tokens = new Map()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
issue(value) {
|
|
263
|
+
this.purge()
|
|
264
|
+
const token = this.randomToken()
|
|
265
|
+
const expiresAt = this.now() + this.ttlMs
|
|
266
|
+
this.tokens.set(token, { ...value, expiresAt })
|
|
267
|
+
return { token, expiresAt }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
consume(token, { ownerSessionId, projectRoot }) {
|
|
271
|
+
const stored = this.tokens.get(token)
|
|
272
|
+
if (!stored) throw new Error('SESSION_SELECTION_TOKEN_INVALID: preview again before running the diagnostic')
|
|
273
|
+
// Consume before validation so a stolen or failed token cannot be replayed.
|
|
274
|
+
this.tokens.delete(token)
|
|
275
|
+
if (stored.ownerSessionId !== ownerSessionId) {
|
|
276
|
+
throw new Error('SESSION_SELECTION_TOKEN_OWNER_MISMATCH: the token belongs to another Agent Session')
|
|
277
|
+
}
|
|
278
|
+
if (path.resolve(stored.projectRoot) !== path.resolve(projectRoot)) {
|
|
279
|
+
throw new Error('SESSION_SELECTION_TOKEN_WORKSPACE_MISMATCH: the token belongs to another workspace')
|
|
280
|
+
}
|
|
281
|
+
if (stored.expiresAt <= this.now()) {
|
|
282
|
+
throw new Error('SESSION_SELECTION_TOKEN_EXPIRED: preview again before running the diagnostic')
|
|
283
|
+
}
|
|
284
|
+
this.purge()
|
|
285
|
+
return stored
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
purge() {
|
|
289
|
+
const now = this.now()
|
|
290
|
+
for (const [token, value] of this.tokens) {
|
|
291
|
+
if (value.expiresAt <= now) this.tokens.delete(token)
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
package/lib/setup.js
CHANGED
|
@@ -5,14 +5,14 @@ import process from 'node:process'
|
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
|
|
7
7
|
import { runProcess } from './process.js'
|
|
8
|
-
|
|
9
|
-
export const DSH_VERSION = '0.1.0-rc.6'
|
|
8
|
+
import { DSH_RUNTIME_VERSION } from './runtime-identity.js'
|
|
10
9
|
|
|
11
10
|
const packageJson = JSON.parse(
|
|
12
11
|
await readFile(new URL('../package.json', import.meta.url), 'utf8'),
|
|
13
12
|
)
|
|
14
13
|
|
|
15
14
|
export const INTEGRATION_VERSION = packageJson.version
|
|
15
|
+
export const DSH_VERSION = DSH_RUNTIME_VERSION
|
|
16
16
|
|
|
17
17
|
function requireValue(args, index, flag) {
|
|
18
18
|
const value = args[index + 1]
|
|
@@ -281,9 +281,7 @@ export async function setupIntegration(raw = {}, dependencies = {}) {
|
|
|
281
281
|
const patchChanged = await writeProfilePatch(config.patchFile, config)
|
|
282
282
|
const harborVersion = await run(config.harborBin, ['--version'], { timeoutMs: 10_000 })
|
|
283
283
|
const plugins = await run(config.harborBin, ['plugins', 'list'], { timeoutMs: 10_000 })
|
|
284
|
-
|
|
285
|
-
throw new Error('Harbor installed, but its dsh-evolution plugin entry point was not discovered')
|
|
286
|
-
}
|
|
284
|
+
verifyHarborPlugins(plugins.stdout)
|
|
287
285
|
await run(config.harborDshBin, ['--help'], { timeoutMs: 10_000 })
|
|
288
286
|
|
|
289
287
|
return {
|
|
@@ -295,6 +293,14 @@ export async function setupIntegration(raw = {}, dependencies = {}) {
|
|
|
295
293
|
}
|
|
296
294
|
}
|
|
297
295
|
|
|
296
|
+
export function verifyHarborPlugins(output) {
|
|
297
|
+
const missing = ['dsh-evolution', 'dsh-historical-evaluation']
|
|
298
|
+
.filter(name => !String(output).includes(name))
|
|
299
|
+
if (missing.length) {
|
|
300
|
+
throw new Error(`Harbor installed, but these plugin entry points were not discovered: ${missing.join(', ')}`)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
298
304
|
function shellQuote(value) {
|
|
299
305
|
return `'${String(value).replaceAll("'", `'"'"'`)}'`
|
|
300
306
|
}
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export const NPM_PACKAGE_NAME = 'dsh-harbor-evolution'
|
|
2
|
+
export const NPM_LATEST_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`
|
|
3
|
+
export const RELEASES_URL = 'https://github.com/istarwyh/harbor-self-evolving/releases'
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CACHE_TTL_MS = 6 * 60 * 60 * 1_000
|
|
6
|
+
const DEFAULT_FAILURE_TTL_MS = 5 * 60 * 1_000
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 2_500
|
|
8
|
+
|
|
9
|
+
export function parseSemver(value) {
|
|
10
|
+
const match = String(value ?? '').match(/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/)
|
|
11
|
+
if (!match) return undefined
|
|
12
|
+
return {
|
|
13
|
+
major: Number(match[1]),
|
|
14
|
+
minor: Number(match[2]),
|
|
15
|
+
patch: Number(match[3]),
|
|
16
|
+
prerelease: match[4] ? match[4].split('.') : [],
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function compareIdentifier(left, right) {
|
|
21
|
+
const leftNumber = /^\d+$/.test(left) ? Number(left) : undefined
|
|
22
|
+
const rightNumber = /^\d+$/.test(right) ? Number(right) : undefined
|
|
23
|
+
if (leftNumber !== undefined && rightNumber !== undefined) return Math.sign(leftNumber - rightNumber)
|
|
24
|
+
if (leftNumber !== undefined) return -1
|
|
25
|
+
if (rightNumber !== undefined) return 1
|
|
26
|
+
return left.localeCompare(right)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function compareSemver(leftValue, rightValue) {
|
|
30
|
+
const left = parseSemver(leftValue)
|
|
31
|
+
const right = parseSemver(rightValue)
|
|
32
|
+
if (!left || !right) return undefined
|
|
33
|
+
for (const key of ['major', 'minor', 'patch']) {
|
|
34
|
+
if (left[key] !== right[key]) return Math.sign(left[key] - right[key])
|
|
35
|
+
}
|
|
36
|
+
if (!left.prerelease.length && !right.prerelease.length) return 0
|
|
37
|
+
if (!left.prerelease.length) return 1
|
|
38
|
+
if (!right.prerelease.length) return -1
|
|
39
|
+
const length = Math.max(left.prerelease.length, right.prerelease.length)
|
|
40
|
+
for (let index = 0; index < length; index += 1) {
|
|
41
|
+
if (left.prerelease[index] === undefined) return -1
|
|
42
|
+
if (right.prerelease[index] === undefined) return 1
|
|
43
|
+
const comparison = compareIdentifier(left.prerelease[index], right.prerelease[index])
|
|
44
|
+
if (comparison) return Math.sign(comparison)
|
|
45
|
+
}
|
|
46
|
+
return 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function shellQuote(value) {
|
|
50
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function renderUpdateCommand(latestVersion, projectRoot) {
|
|
54
|
+
if (!parseSemver(latestVersion)) throw new Error('latestVersion must be a valid semantic version')
|
|
55
|
+
return `npx --yes ${NPM_PACKAGE_NAME}@${latestVersion} setup --project-root ${shellQuote(projectRoot)}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function buildResult(currentVersion, latestVersion, projectRoot, checkedAt, options = {}) {
|
|
59
|
+
const comparison = compareSemver(currentVersion, latestVersion)
|
|
60
|
+
if (comparison === undefined) {
|
|
61
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: options.source, stale: options.stale }
|
|
62
|
+
}
|
|
63
|
+
const updateAvailable = comparison < 0
|
|
64
|
+
return {
|
|
65
|
+
status: updateAvailable ? 'update-available' : 'up-to-date',
|
|
66
|
+
currentVersion,
|
|
67
|
+
latestVersion,
|
|
68
|
+
checkedAt,
|
|
69
|
+
source: options.source,
|
|
70
|
+
stale: options.stale,
|
|
71
|
+
releaseUrl: updateAvailable ? `${RELEASES_URL}/tag/v${latestVersion}` : RELEASES_URL,
|
|
72
|
+
command: updateAvailable ? renderUpdateCommand(latestVersion, projectRoot) : undefined,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function createVersionChecker(options = {}) {
|
|
77
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch
|
|
78
|
+
const now = options.now ?? Date.now
|
|
79
|
+
const cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS
|
|
80
|
+
const failureTtlMs = options.failureTtlMs ?? DEFAULT_FAILURE_TTL_MS
|
|
81
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS
|
|
82
|
+
let successful
|
|
83
|
+
let failedAt = 0
|
|
84
|
+
|
|
85
|
+
return async function checkVersion({ currentVersion, projectRoot, refresh = false }) {
|
|
86
|
+
const checkedAt = new Date(now()).toISOString()
|
|
87
|
+
if (!refresh && successful && now() < successful.expiresAt) {
|
|
88
|
+
return buildResult(currentVersion, successful.latestVersion, projectRoot, successful.checkedAt, { source: 'cache' })
|
|
89
|
+
}
|
|
90
|
+
if (!refresh && !successful && failedAt && now() - failedAt < failureTtlMs) {
|
|
91
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: 'cache' }
|
|
92
|
+
}
|
|
93
|
+
if (typeof fetchImpl !== 'function') {
|
|
94
|
+
failedAt = now()
|
|
95
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: 'host' }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const controller = new AbortController()
|
|
99
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs)
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetchImpl(NPM_LATEST_URL, {
|
|
102
|
+
headers: { accept: 'application/json' },
|
|
103
|
+
redirect: 'error',
|
|
104
|
+
signal: controller.signal,
|
|
105
|
+
})
|
|
106
|
+
if (!response.ok) throw new Error('registry request failed')
|
|
107
|
+
const manifest = await response.json()
|
|
108
|
+
if (manifest?.name !== NPM_PACKAGE_NAME || !parseSemver(manifest.version)) {
|
|
109
|
+
throw new Error('registry response is invalid')
|
|
110
|
+
}
|
|
111
|
+
successful = {
|
|
112
|
+
latestVersion: manifest.version,
|
|
113
|
+
checkedAt,
|
|
114
|
+
expiresAt: now() + cacheTtlMs,
|
|
115
|
+
}
|
|
116
|
+
failedAt = 0
|
|
117
|
+
return buildResult(currentVersion, successful.latestVersion, projectRoot, checkedAt, { source: 'registry' })
|
|
118
|
+
} catch {
|
|
119
|
+
failedAt = now()
|
|
120
|
+
if (successful) {
|
|
121
|
+
return buildResult(currentVersion, successful.latestVersion, projectRoot, successful.checkedAt, { source: 'cache', stale: true })
|
|
122
|
+
}
|
|
123
|
+
return { status: 'unavailable', currentVersion, checkedAt, source: 'registry' }
|
|
124
|
+
} finally {
|
|
125
|
+
clearTimeout(timeout)
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
package/lib/web.js
CHANGED
|
@@ -8,6 +8,8 @@ export const COMPARE_ROUTE = '/_dsh/harbor-evolution/compare'
|
|
|
8
8
|
export const GOVERNANCE_ROUTE = '/_dsh/harbor-evolution/governance'
|
|
9
9
|
export const EVALUATOR_ROUTE = '/_dsh/harbor-evolution/evaluator'
|
|
10
10
|
export const META_ROUTE = '/_dsh/harbor-evolution/meta'
|
|
11
|
+
export const PROJECT_ROOT_ROUTE = '/_dsh/harbor-evolution/project-root'
|
|
12
|
+
export const VERSION_ROUTE = '/_dsh/harbor-evolution/version'
|
|
11
13
|
const MAX_MUTATION_BYTES = 256 * 1024
|
|
12
14
|
|
|
13
15
|
function sendJson(response, status, body) {
|
|
@@ -64,7 +66,7 @@ export function createApiHandler(load, code = 'request-failed') {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
export function createDashboardHandler(service) {
|
|
67
|
-
return createApiHandler(
|
|
69
|
+
return createApiHandler(args => service.dashboard(args), 'dashboard-unavailable')
|
|
68
70
|
}
|
|
69
71
|
|
|
70
72
|
export function createMutationHandler(update, code = 'update-failed') {
|
|
@@ -116,6 +118,8 @@ export function installDashboardWeb(ctx, service) {
|
|
|
116
118
|
[GOVERNANCE_ROUTE, createApiHandler(args => service.governance(args), 'governance-unavailable')],
|
|
117
119
|
[EVALUATOR_ROUTE, createMutationHandler(args => service.evaluator(args), 'evaluator-update-failed')],
|
|
118
120
|
[META_ROUTE, createApiHandler(args => service.meta(args), 'meta-evaluation-unavailable')],
|
|
121
|
+
[VERSION_ROUTE, createApiHandler(args => service.version(args), 'version-check-unavailable')],
|
|
122
|
+
[PROJECT_ROOT_ROUTE, createMutationHandler(args => service.setProjectRoot(args), 'project-root-update-failed')],
|
|
119
123
|
]
|
|
120
124
|
for (const [route, handler] of routes) {
|
|
121
125
|
webCtx.effect(() => webCtx.webServer.register({ kind: 'exact', path: route, handler }), `harbor-evolution: ${route}`)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-harbor-evolution",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "DeepSeek Harness plugin and bundled Skill for safely evolving Cordis Candidates with Harbor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
"./client": "./lib/client.js",
|
|
10
10
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
11
11
|
"./schemas/evaluation-result.schema.json": "./schemas/evaluation-result.schema.json",
|
|
12
|
+
"./schemas/evaluation-result-v2.schema.json": "./schemas/evaluation-result-v2.schema.json",
|
|
13
|
+
"./schemas/historical-generation-batch.schema.json": "./schemas/historical-generation-batch.schema.json",
|
|
14
|
+
"./schemas/dsh-session-observation.schema.json": "./schemas/dsh-session-observation.schema.json",
|
|
15
|
+
"./schemas/historical-evaluation-context.schema.json": "./schemas/historical-evaluation-context.schema.json",
|
|
16
|
+
"./schemas/historical-evaluation-summary.schema.json": "./schemas/historical-evaluation-summary.schema.json",
|
|
12
17
|
"./schemas/ground-truth.schema.json": "./schemas/ground-truth.schema.json",
|
|
13
18
|
"./schemas/evaluator-observations.schema.json": "./schemas/evaluator-observations.schema.json",
|
|
14
19
|
"./schemas/meta-evaluation-report.schema.json": "./schemas/meta-evaluation-report.schema.json",
|
|
@@ -48,9 +53,14 @@
|
|
|
48
53
|
"platform": "web"
|
|
49
54
|
}
|
|
50
55
|
},
|
|
56
|
+
"harborEvolution": {
|
|
57
|
+
"runtimePolicy": "follow-latest",
|
|
58
|
+
"dshRuntimeVersion": "latest",
|
|
59
|
+
"candidateAcpPackage": "@deepseek-ai/dsh-acp-demo@latest"
|
|
60
|
+
},
|
|
51
61
|
"peerDependencies": {
|
|
52
|
-
"@deepseek-ai/dsh-skill": "
|
|
53
|
-
"@deepseek-ai/dsh-tools": "
|
|
62
|
+
"@deepseek-ai/dsh-skill": ">=0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/dsh-tools": ">=0.1.0-rc.6"
|
|
54
64
|
},
|
|
55
65
|
"dependencies": {
|
|
56
66
|
"@deepseek-ai/schemastery": "3.18.1"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/istarwyh/harbor-self-evolving/schemas/dsh-session-observation.schema.json",
|
|
4
|
+
"title": "DSH Session Observation v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "protocol", "record_kind", "execution_mode", "trial_id", "source", "generator", "task", "visible_transcript", "execution", "feedback", "completeness", "redaction", "digest"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": 1 },
|
|
10
|
+
"protocol": { "const": "dsh-session-observation/v1" },
|
|
11
|
+
"record_kind": { "const": "dsh-session" },
|
|
12
|
+
"execution_mode": { "const": "observe-existing" },
|
|
13
|
+
"trial_id": { "type": "string", "minLength": 1 },
|
|
14
|
+
"source": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"required": ["ref", "captured_through_seq", "source_digest", "created_at", "last_activity_at", "last_turn_reason", "session_format_version"],
|
|
17
|
+
"properties": {
|
|
18
|
+
"ref": { "$ref": "#/$defs/digest" },
|
|
19
|
+
"captured_through_seq": { "type": "integer", "minimum": 0 },
|
|
20
|
+
"source_digest": { "$ref": "#/$defs/digest" },
|
|
21
|
+
"created_at": { "type": ["string", "null"], "format": "date-time" },
|
|
22
|
+
"last_activity_at": { "type": ["string", "null"], "format": "date-time" },
|
|
23
|
+
"last_turn_reason": { "type": ["string", "null"] },
|
|
24
|
+
"session_format_version": { "type": "integer", "minimum": 0 }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"generator": { "type": "object", "required": ["agent_preset", "model_segments"] },
|
|
28
|
+
"task": {
|
|
29
|
+
"type": "object",
|
|
30
|
+
"required": ["title", "initial_user_goal", "turn_count"],
|
|
31
|
+
"properties": {
|
|
32
|
+
"title": { "type": "string", "minLength": 1 },
|
|
33
|
+
"initial_user_goal": { "type": "string", "minLength": 1 },
|
|
34
|
+
"turn_count": { "type": "integer", "minimum": 1 }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"visible_transcript": {
|
|
38
|
+
"type": "array",
|
|
39
|
+
"minItems": 2,
|
|
40
|
+
"items": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": ["event_seq", "message_ref", "role", "content", "time"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"event_seq": { "type": "integer", "minimum": 0 },
|
|
46
|
+
"message_ref": { "$ref": "#/$defs/digest" },
|
|
47
|
+
"role": { "enum": ["user", "assistant"] },
|
|
48
|
+
"content": {
|
|
49
|
+
"type": "array",
|
|
50
|
+
"minItems": 1,
|
|
51
|
+
"items": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"additionalProperties": false,
|
|
54
|
+
"required": ["type", "text"],
|
|
55
|
+
"properties": { "type": { "const": "text" }, "text": { "type": "string", "minLength": 1 } }
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"time": { "type": ["string", "null"], "format": "date-time" }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
"execution": { "type": "object", "required": ["tools", "turns", "usage"] },
|
|
63
|
+
"feedback": { "type": "object", "required": ["items"] },
|
|
64
|
+
"completeness": { "type": "object", "required": ["transcript_complete", "tool_payloads_complete", "attachments_complete", "truncations"] },
|
|
65
|
+
"redaction": { "type": "object", "required": ["replacements", "truncations", "omitted_blocks"] },
|
|
66
|
+
"digest": { "$ref": "#/$defs/digest" }
|
|
67
|
+
},
|
|
68
|
+
"$defs": { "digest": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$" } }
|
|
69
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://github.com/istarwyh/harbor-self-evolving/schemas/evaluation-result-v2.schema.json",
|
|
4
|
+
"title": "Harbor DSH Evaluation Result v2",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": ["schema_version", "protocol", "criteria", "aggregate"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schema_version": { "const": 2 },
|
|
10
|
+
"protocol": { "const": "evaluation-result/v2" },
|
|
11
|
+
"criteria": {
|
|
12
|
+
"type": "array",
|
|
13
|
+
"minItems": 1,
|
|
14
|
+
"items": {
|
|
15
|
+
"type": "object",
|
|
16
|
+
"additionalProperties": false,
|
|
17
|
+
"required": ["id", "status", "score", "reason", "recommendation", "evidence_refs"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"id": { "type": "string", "minLength": 1 },
|
|
20
|
+
"status": { "enum": ["scored", "not-applicable", "insufficient-evidence", "evaluation-error"] },
|
|
21
|
+
"score": { "type": ["number", "null"], "enum": [0, 0.5, 1, null] },
|
|
22
|
+
"reason": { "type": "string", "minLength": 1 },
|
|
23
|
+
"recommendation": { "type": "string", "minLength": 1 },
|
|
24
|
+
"evidence_refs": { "type": "array", "items": { "type": "string", "minLength": 1 } }
|
|
25
|
+
},
|
|
26
|
+
"allOf": [
|
|
27
|
+
{ "if": { "properties": { "status": { "const": "scored" } } }, "then": { "properties": { "score": { "enum": [0, 0.5, 1] } } } },
|
|
28
|
+
{ "if": { "properties": { "status": { "enum": ["not-applicable", "insufficient-evidence", "evaluation-error"] } } }, "then": { "properties": { "score": { "type": "null" } } } }
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"aggregate": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"additionalProperties": false,
|
|
35
|
+
"required": ["metric_id", "value", "scored_criteria", "total_criteria", "coverage"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"metric_id": { "type": "string", "minLength": 1 },
|
|
38
|
+
"value": { "type": ["number", "null"], "minimum": 0, "maximum": 1 },
|
|
39
|
+
"scored_criteria": { "type": "integer", "minimum": 0 },
|
|
40
|
+
"total_criteria": { "type": "integer", "minimum": 1 },
|
|
41
|
+
"coverage": { "type": "number", "minimum": 0, "maximum": 1 }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|