mio-agent-runtime 0.5.16 → 0.5.22
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 +163 -107
- package/adapters/claude-code.js +141 -0
- package/bin/mio.js +493 -245
- package/observe/observer.js +286 -2
- package/package.json +26 -24
- package/server/creativity-engine.js +397 -0
- package/server/digest.js +266 -0
- package/server/host-capabilities.js +5 -0
- package/server/memory-store.js +376 -0
- package/server/mio-intelligence-mcp/README.md +2 -2
- package/server/mio-intelligence-mcp/index.js +2325 -1966
- package/server/retention.js +150 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const crypto = require('crypto')
|
|
6
|
+
|
|
7
|
+
// ═══════════════════════════════════════════════
|
|
8
|
+
// JSONL Store — ideas, combos, experiments
|
|
9
|
+
// ═══════════════════════════════════════════════
|
|
10
|
+
|
|
11
|
+
class CreativityStore {
|
|
12
|
+
constructor(dataDir) {
|
|
13
|
+
this.dataDir = dataDir
|
|
14
|
+
this.hypothesesPath = path.join(dataDir, 'creativity-hypotheses.jsonl')
|
|
15
|
+
this.combosPath = path.join(dataDir, 'creativity-combos.jsonl')
|
|
16
|
+
this.experimentsPath = path.join(dataDir, 'creativity-experiments.jsonl')
|
|
17
|
+
this._hypotheses = null
|
|
18
|
+
this._combos = null
|
|
19
|
+
this._experiments = null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
_ensureDir() {
|
|
23
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
24
|
+
fs.mkdirSync(this.dataDir, { recursive: true })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
_readJsonl(filePath) {
|
|
29
|
+
if (!fs.existsSync(filePath)) return []
|
|
30
|
+
return fs.readFileSync(filePath, 'utf-8')
|
|
31
|
+
.split('\n')
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.map(line => {
|
|
34
|
+
try { return JSON.parse(line) } catch { return null }
|
|
35
|
+
})
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
_appendJsonl(filePath, record) {
|
|
40
|
+
this._ensureDir()
|
|
41
|
+
fs.appendFileSync(filePath, JSON.stringify(record) + '\n')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
getHypotheses(opts = {}) {
|
|
45
|
+
if (this._hypotheses === null) {
|
|
46
|
+
this._hypotheses = this._readJsonl(this.hypothesesPath)
|
|
47
|
+
}
|
|
48
|
+
let list = this._hypotheses
|
|
49
|
+
if (opts.status) list = list.filter(h => h.status === opts.status)
|
|
50
|
+
if (opts.limit) list = list.slice(-opts.limit)
|
|
51
|
+
return list
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
addHypothesis(h) {
|
|
55
|
+
this._appendJsonl(this.hypothesesPath, h)
|
|
56
|
+
if (this._hypotheses) this._hypotheses.push(h)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
updateHypothesis(id, patch) {
|
|
60
|
+
const all = this.getHypotheses()
|
|
61
|
+
const idx = all.findIndex(h => h.id === id)
|
|
62
|
+
if (idx === -1) return false
|
|
63
|
+
Object.assign(all[idx], patch)
|
|
64
|
+
this._hypotheses = all
|
|
65
|
+
this._rewriteFile(this.hypothesesPath, all)
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getCombos(limit) {
|
|
70
|
+
if (this._combos === null) {
|
|
71
|
+
this._combos = this._readJsonl(this.combosPath)
|
|
72
|
+
}
|
|
73
|
+
return limit ? this._combos.slice(-limit) : this._combos
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
addCombo(c) {
|
|
77
|
+
this._appendJsonl(this.combosPath, c)
|
|
78
|
+
if (this._combos) this._combos.push(c)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getExperiments() {
|
|
82
|
+
if (this._experiments === null) {
|
|
83
|
+
this._experiments = this._readJsonl(this.experimentsPath)
|
|
84
|
+
}
|
|
85
|
+
return this._experiments
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
addExperiment(e) {
|
|
89
|
+
this._appendJsonl(this.experimentsPath, e)
|
|
90
|
+
if (this._experiments) this._experiments.push(e)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
count() {
|
|
94
|
+
return {
|
|
95
|
+
hypotheses: this.getHypotheses().length,
|
|
96
|
+
combos: this.getCombos().length,
|
|
97
|
+
experiments: this.getExperiments().length,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
_rewriteFile(filePath, records) {
|
|
102
|
+
this._ensureDir()
|
|
103
|
+
fs.writeFileSync(filePath, records.map(r => JSON.stringify(r)).join('\n') + '\n')
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ═══════════════════════════════════════════════
|
|
108
|
+
// Concept Mixer — pair sources for novel combos
|
|
109
|
+
// ═══════════════════════════════════════════════
|
|
110
|
+
|
|
111
|
+
function mulberry32(seed) {
|
|
112
|
+
let t = seed | 0
|
|
113
|
+
return () => {
|
|
114
|
+
t = (t + 0x6D2B79F5) | 0
|
|
115
|
+
let x = Math.imul(t ^ (t >>> 15), 1 | t)
|
|
116
|
+
x ^= x + Math.imul(x ^ (x >>> 7), 61 | x)
|
|
117
|
+
return ((x ^ (x >>> 14)) >>> 0) / 4294967296
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function mixConcepts(sources, exploredPairs, rng) {
|
|
122
|
+
if (sources.length < 2) return []
|
|
123
|
+
const pairs = []
|
|
124
|
+
for (let i = 0; i < sources.length; i++) {
|
|
125
|
+
for (let j = i + 1; j < sources.length; j++) {
|
|
126
|
+
const key = `${sources[i].name}||${sources[j].name}`
|
|
127
|
+
const reverseKey = `${sources[j].name}||${sources[i].name}`
|
|
128
|
+
if (exploredPairs.has(key) || exploredPairs.has(reverseKey)) continue
|
|
129
|
+
pairs.push([sources[i], sources[j]])
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Shuffle and take top pairs
|
|
133
|
+
for (let i = pairs.length - 1; i > 0; i--) {
|
|
134
|
+
const j = Math.floor(rng() * (i + 1))
|
|
135
|
+
;[pairs[i], pairs[j]] = [pairs[j], pairs[i]]
|
|
136
|
+
}
|
|
137
|
+
return pairs.slice(0, Math.min(5, pairs.length))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ═══════════════════════════════════════════════
|
|
141
|
+
// Novelty Scorer — reject duplicates
|
|
142
|
+
// ═══════════════════════════════════════════════
|
|
143
|
+
|
|
144
|
+
function bigramSet(text) {
|
|
145
|
+
const s = new Set()
|
|
146
|
+
const lower = text.toLowerCase()
|
|
147
|
+
for (let i = 0; i < lower.length - 1; i++) {
|
|
148
|
+
s.add(lower.slice(i, i + 2))
|
|
149
|
+
}
|
|
150
|
+
return s
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function jaccard(a, b) {
|
|
154
|
+
if (a.size === 0 && b.size === 0) return 1
|
|
155
|
+
let inter = 0
|
|
156
|
+
for (const x of a) if (b.has(x)) inter++
|
|
157
|
+
return inter / (a.size + b.size - inter)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function evaluateNovelty(idea, recent, rejected) {
|
|
161
|
+
const ideaBigrams = bigramSet(idea.title + ' ' + idea.idea)
|
|
162
|
+
for (const r of recent) {
|
|
163
|
+
const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
|
|
164
|
+
if (sim > 0.7) {
|
|
165
|
+
return { shouldReject: true, rejectReason: `too similar to recent: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
for (const r of rejected) {
|
|
169
|
+
const sim = jaccard(ideaBigrams, bigramSet(r.title + ' ' + r.idea))
|
|
170
|
+
if (sim > 0.6) {
|
|
171
|
+
return { shouldReject: true, rejectReason: `matches rejected: "${r.title}" (sim=${sim.toFixed(2)})`, adjustedNovelty: idea.novelty }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { shouldReject: false, adjustedNovelty: Math.min(100, idea.novelty + 5) }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ═══════════════════════════════════════════════
|
|
178
|
+
// Creativity Engine — main orchestrator
|
|
179
|
+
// ═══════════════════════════════════════════════
|
|
180
|
+
|
|
181
|
+
const STRATEGY_CYCLE = ['explore', 'signal', 'stable']
|
|
182
|
+
|
|
183
|
+
class CreativityEngine {
|
|
184
|
+
constructor(dataDir, chatJson) {
|
|
185
|
+
this.store = new CreativityStore(dataDir)
|
|
186
|
+
this.chatJson = chatJson
|
|
187
|
+
this.strategyIndex = 0
|
|
188
|
+
this.exploredPairs = new Set()
|
|
189
|
+
this.rng = mulberry32(Date.now())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async generate(sourceLabels, strategy) {
|
|
193
|
+
const sources = sourceLabels.map(s => ({
|
|
194
|
+
name: s.name || 'unnamed',
|
|
195
|
+
content: s.content || '',
|
|
196
|
+
type: s.type || 'knowledge',
|
|
197
|
+
weight: s.weight || 0.5,
|
|
198
|
+
}))
|
|
199
|
+
|
|
200
|
+
if (sources.length < 2) {
|
|
201
|
+
return { ideas: [], reason: 'need at least 2 sources' }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const strat = strategy || STRATEGY_CYCLE[this.strategyIndex % STRATEGY_CYCLE.length]
|
|
205
|
+
this.strategyIndex++
|
|
206
|
+
|
|
207
|
+
// Mix concepts
|
|
208
|
+
const recent = this.store.getHypotheses({ limit: 30 })
|
|
209
|
+
const rejected = this.store.getHypotheses({ status: 'rejected', limit: 50 })
|
|
210
|
+
const exploredPairs = new Set(recent.flatMap(h => (h.sourceLabels || []).map((l, i, a) => i > 0 ? `${a[i-1]}||${l}` : '')).filter(Boolean))
|
|
211
|
+
const pairs = mixConcepts(sources, exploredPairs, this.rng.bind(this))
|
|
212
|
+
|
|
213
|
+
if (pairs.length === 0) {
|
|
214
|
+
return { ideas: [], reason: 'all pairs already explored' }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Generate hypotheses via LLM
|
|
218
|
+
const ideas = []
|
|
219
|
+
for (const [a, b] of pairs) {
|
|
220
|
+
const prompt = this._buildGenerationPrompt(a, b, strat, recent.slice(-10), rejected.slice(-5))
|
|
221
|
+
try {
|
|
222
|
+
const result = await this.chatJson(prompt, {
|
|
223
|
+
system: `You are a creative hypothesis generator. Output JSON with fields: title, idea, expectedBenefit, risk, novelty (0-100), feasibility (0-100), impact (0-100). Generate ONE novel hypothesis by combining the given concepts.`,
|
|
224
|
+
temperature: strat === 'explore' ? 0.8 : 0.4,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
if (result.data) {
|
|
228
|
+
const h = {
|
|
229
|
+
id: crypto.randomUUID(),
|
|
230
|
+
title: result.data.title || 'Untitled',
|
|
231
|
+
idea: result.data.idea || '',
|
|
232
|
+
expectedBenefit: result.data.expectedBenefit || '',
|
|
233
|
+
risk: result.data.risk || '',
|
|
234
|
+
novelty: Math.min(100, Math.max(0, result.data.novelty || 50)),
|
|
235
|
+
feasibility: Math.min(100, Math.max(0, result.data.feasibility || 50)),
|
|
236
|
+
impact: Math.min(100, Math.max(0, result.data.impact || 50)),
|
|
237
|
+
sourceLabels: [a.name, b.name],
|
|
238
|
+
status: 'active',
|
|
239
|
+
createdAt: Date.now(),
|
|
240
|
+
strategy: strat,
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Novelty check
|
|
244
|
+
const verdict = evaluateNovelty(h, recent, rejected)
|
|
245
|
+
if (verdict.shouldReject) {
|
|
246
|
+
h.status = 'rejected'
|
|
247
|
+
h.rejectionReason = verdict.rejectReason
|
|
248
|
+
} else {
|
|
249
|
+
h.novelty = verdict.adjustedNovelty
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
this.store.addHypothesis(h)
|
|
253
|
+
ideas.push(h)
|
|
254
|
+
|
|
255
|
+
// Record combo
|
|
256
|
+
this.store.addCombo({
|
|
257
|
+
id: crypto.randomUUID(),
|
|
258
|
+
sources: [a.name, b.name],
|
|
259
|
+
description: h.title,
|
|
260
|
+
createdAt: Date.now(),
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
} catch (err) {
|
|
264
|
+
// LLM call failed, skip this pair
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { ideas, strategy: strat, pairsAttempted: pairs.length }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async ferment(limit = 5) {
|
|
272
|
+
const fermentable = this.store.getHypotheses({ status: 'active' })
|
|
273
|
+
.concat(this.store.getHypotheses({ status: 'draft' }))
|
|
274
|
+
.slice(-limit)
|
|
275
|
+
|
|
276
|
+
if (fermentable.length === 0) {
|
|
277
|
+
return { fermented: 0, reason: 'no fermentable hypotheses' }
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const results = []
|
|
281
|
+
for (const h of fermentable) {
|
|
282
|
+
const prompt = `Review and refine this hypothesis. Current state:
|
|
283
|
+
Title: ${h.title}
|
|
284
|
+
Idea: ${h.idea}
|
|
285
|
+
Novelty: ${h.novelty}, Feasibility: ${h.feasibility}, Impact: ${h.impact}
|
|
286
|
+
|
|
287
|
+
Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impact, verdict (promote/keep/reject/merge), reason.`
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
const result = await this.chatJson(prompt, {
|
|
291
|
+
system: 'You are a creative hypothesis reviewer. Output JSON with refined scores and a verdict.',
|
|
292
|
+
temperature: 0.3,
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
if (result.data) {
|
|
296
|
+
const patch = {
|
|
297
|
+
title: result.data.title || h.title,
|
|
298
|
+
idea: result.data.idea || h.idea,
|
|
299
|
+
expectedBenefit: result.data.expectedBenefit || h.expectedBenefit,
|
|
300
|
+
risk: result.data.risk || h.risk,
|
|
301
|
+
novelty: Math.min(100, Math.max(0, result.data.novelty || h.novelty)),
|
|
302
|
+
feasibility: Math.min(100, Math.max(0, result.data.feasibility || h.feasibility)),
|
|
303
|
+
impact: Math.min(100, Math.max(0, result.data.impact || h.impact)),
|
|
304
|
+
fermentedAt: Date.now(),
|
|
305
|
+
fermentCount: (h.fermentCount || 0) + 1,
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (result.data.verdict === 'reject') {
|
|
309
|
+
patch.status = 'rejected'
|
|
310
|
+
} else if (result.data.verdict === 'promote' && (h.novelty + h.feasibility + h.impact) > 200) {
|
|
311
|
+
patch.status = 'validated'
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.store.updateHypothesis(h.id, patch)
|
|
315
|
+
results.push({ id: h.id, title: h.title, verdict: result.data.verdict, reason: result.data.reason })
|
|
316
|
+
}
|
|
317
|
+
} catch (err) {
|
|
318
|
+
// skip
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return { fermented: results.length, results }
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
status() {
|
|
326
|
+
const count = this.store.count()
|
|
327
|
+
const active = this.store.getHypotheses({ status: 'active' })
|
|
328
|
+
const validated = this.store.getHypotheses({ status: 'validated' })
|
|
329
|
+
const rejected = this.store.getHypotheses({ status: 'rejected' })
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
...count,
|
|
333
|
+
active: active.length,
|
|
334
|
+
validated: validated.length,
|
|
335
|
+
rejected: rejected.length,
|
|
336
|
+
recentIdeas: active.slice(-5).map(h => ({
|
|
337
|
+
id: h.id,
|
|
338
|
+
title: h.title,
|
|
339
|
+
novelty: h.novelty,
|
|
340
|
+
feasibility: h.feasibility,
|
|
341
|
+
impact: h.impact,
|
|
342
|
+
score: h.novelty + h.feasibility + h.impact,
|
|
343
|
+
})),
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
list(opts = {}) {
|
|
348
|
+
return this.store.getHypotheses(opts).map(h => ({
|
|
349
|
+
id: h.id,
|
|
350
|
+
title: h.title,
|
|
351
|
+
idea: h.idea,
|
|
352
|
+
status: h.status,
|
|
353
|
+
novelty: h.novelty,
|
|
354
|
+
feasibility: h.feasibility,
|
|
355
|
+
impact: h.impact,
|
|
356
|
+
score: h.novelty + h.feasibility + h.impact,
|
|
357
|
+
sourceLabels: h.sourceLabels,
|
|
358
|
+
createdAt: h.createdAt,
|
|
359
|
+
fermentedAt: h.fermentedAt,
|
|
360
|
+
fermentCount: h.fermentCount,
|
|
361
|
+
}))
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
|
|
365
|
+
let prompt = `Combine these two concepts into ONE novel hypothesis:\n\n`
|
|
366
|
+
prompt += `Concept A: "${a.name}" — ${a.content}\n`
|
|
367
|
+
prompt += `Concept B: "${b.name}" — ${b.content}\n\n`
|
|
368
|
+
prompt += `Strategy: ${strategy}\n`
|
|
369
|
+
|
|
370
|
+
if (strategy === 'explore') {
|
|
371
|
+
prompt += `Cross-domain combination. Emphasize structural differences and unexpected connections.\n`
|
|
372
|
+
} else if (strategy === 'signal') {
|
|
373
|
+
prompt += `Inject external signals. Be provocative and challenge assumptions.\n`
|
|
374
|
+
} else {
|
|
375
|
+
prompt += `Stable combination. Conservative refinement of existing ideas.\n`
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (recentHypotheses.length > 0) {
|
|
379
|
+
prompt += `\nRecent ideas (avoid duplicates):\n`
|
|
380
|
+
for (const h of recentHypotheses) {
|
|
381
|
+
prompt += `- "${h.title}" (novelty=${h.novelty})\n`
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (rejectedHypotheses.length > 0) {
|
|
386
|
+
prompt += `\nRejected ideas (learn from failures):\n`
|
|
387
|
+
for (const h of rejectedHypotheses) {
|
|
388
|
+
prompt += `- "${h.title}" — ${h.risk}\n`
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
prompt += `\nOutput JSON: {"title": "...", "idea": "...", "expectedBenefit": "...", "risk": "...", "novelty": 0-100, "feasibility": 0-100, "impact": 0-100}`
|
|
393
|
+
return prompt
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
module.exports = { CreativityEngine, CreativityStore }
|
package/server/digest.js
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Digest: turn the accumulated MIO_HOME data (traces, memory, reuse evidence)
|
|
4
|
+
// into an actionable report. Deterministic aggregation only — no LLM. The
|
|
5
|
+
// report answers: what got done, where the errors are, which agent performs
|
|
6
|
+
// best, and what deserves attention next.
|
|
7
|
+
|
|
8
|
+
const fs = require('fs')
|
|
9
|
+
const path = require('path')
|
|
10
|
+
|
|
11
|
+
function readJsonl(file) {
|
|
12
|
+
if (!fs.existsSync(file)) return []
|
|
13
|
+
return fs
|
|
14
|
+
.readFileSync(file, 'utf8')
|
|
15
|
+
.split(/\r?\n/)
|
|
16
|
+
.filter(Boolean)
|
|
17
|
+
.map((line) => {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(line)
|
|
20
|
+
} catch (_) {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isVerifiedReuse(record) {
|
|
28
|
+
return (
|
|
29
|
+
(record.reuse === true || record.reuse === 'true') &&
|
|
30
|
+
(record.behaviorChanged === true || record.behaviorChanged === 'true') &&
|
|
31
|
+
(record.outcomeImproved === true || record.outcomeImproved === 'true')
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createDigest(options = {}) {
|
|
36
|
+
const home = options.home
|
|
37
|
+
const now = options.now || (() => Date.now())
|
|
38
|
+
|
|
39
|
+
function generate(args = {}) {
|
|
40
|
+
const days = Math.max(1, Number(args.days) || 7)
|
|
41
|
+
const project = args.project ? String(args.project) : null
|
|
42
|
+
const atTime = now()
|
|
43
|
+
const cutoff = atTime - days * 86400000
|
|
44
|
+
|
|
45
|
+
const traces = readJsonl(path.join(home, 'traces.jsonl')).filter((trace) => {
|
|
46
|
+
if (!trace || typeof trace !== 'object') return false
|
|
47
|
+
const ts = Date.parse(trace.timestamp || '')
|
|
48
|
+
if (!(Number.isFinite(ts) && ts >= cutoff)) return false
|
|
49
|
+
if (project && trace.project !== project) return false
|
|
50
|
+
return true
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const outcomes = traces.filter((t) => String(t.event_type || '').toLowerCase() === 'task_outcome')
|
|
54
|
+
const errors = traces.filter((t) => String(t.event_type || '').toLowerCase() === 'error')
|
|
55
|
+
|
|
56
|
+
// --- per-agent performance ---
|
|
57
|
+
const agentMap = new Map()
|
|
58
|
+
for (const outcome of outcomes) {
|
|
59
|
+
const agent = String(outcome.agent || 'unknown')
|
|
60
|
+
let entry = agentMap.get(agent)
|
|
61
|
+
if (!entry) {
|
|
62
|
+
entry = { agent, total: 0, success: 0, failure: 0, aborted: 0 }
|
|
63
|
+
agentMap.set(agent, entry)
|
|
64
|
+
}
|
|
65
|
+
entry.total += 1
|
|
66
|
+
const oc = String(outcome.outcome || 'none').toLowerCase()
|
|
67
|
+
if (oc === 'success') entry.success += 1
|
|
68
|
+
else if (oc === 'failure' || oc === 'error') entry.failure += 1
|
|
69
|
+
else entry.aborted += 1
|
|
70
|
+
}
|
|
71
|
+
const agents = [...agentMap.values()]
|
|
72
|
+
.map((entry) => ({
|
|
73
|
+
...entry,
|
|
74
|
+
successRate: entry.total > 0 ? Math.round((entry.success / entry.total) * 100) : 0,
|
|
75
|
+
}))
|
|
76
|
+
.sort((a, b) => b.total - a.total)
|
|
77
|
+
|
|
78
|
+
// --- error hotspots ---
|
|
79
|
+
const errorByAgent = new Map()
|
|
80
|
+
const errorByTool = new Map()
|
|
81
|
+
for (const error of errors) {
|
|
82
|
+
const agent = String(error.agent || 'unknown')
|
|
83
|
+
errorByAgent.set(agent, (errorByAgent.get(agent) || 0) + 1)
|
|
84
|
+
const tool =
|
|
85
|
+
(error.payload && (error.payload.tool || error.payload.toolUseId || error.payload.status)) ||
|
|
86
|
+
'unknown'
|
|
87
|
+
const key = agent + ':' + String(tool)
|
|
88
|
+
errorByTool.set(key, (errorByTool.get(key) || 0) + 1)
|
|
89
|
+
}
|
|
90
|
+
const errorHotspots = {
|
|
91
|
+
total: errors.length,
|
|
92
|
+
byAgent: [...errorByAgent.entries()]
|
|
93
|
+
.map(([agent, count]) => ({ agent, count }))
|
|
94
|
+
.sort((a, b) => b.count - a.count),
|
|
95
|
+
byTool: [...errorByTool.entries()]
|
|
96
|
+
.map(([key, count]) => ({ key, count }))
|
|
97
|
+
.sort((a, b) => b.count - a.count)
|
|
98
|
+
.slice(0, 10),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// --- project activity ---
|
|
102
|
+
const projectMap = new Map()
|
|
103
|
+
for (const outcome of outcomes) {
|
|
104
|
+
const name = String(outcome.project || 'unknown')
|
|
105
|
+
let entry = projectMap.get(name)
|
|
106
|
+
if (!entry) {
|
|
107
|
+
entry = { project: name, tasks: 0, success: 0, lastAt: null, recent: [] }
|
|
108
|
+
projectMap.set(name, entry)
|
|
109
|
+
}
|
|
110
|
+
entry.tasks += 1
|
|
111
|
+
if (String(outcome.outcome || '').toLowerCase() === 'success') entry.success += 1
|
|
112
|
+
const ts = Date.parse(outcome.timestamp || '')
|
|
113
|
+
if (Number.isFinite(ts) && (!entry.lastAt || ts > entry.lastAt)) entry.lastAt = ts
|
|
114
|
+
const summary = outcome.payload && outcome.payload.summary
|
|
115
|
+
if (summary && entry.recent.length < 3) {
|
|
116
|
+
entry.recent.push({
|
|
117
|
+
at: outcome.timestamp,
|
|
118
|
+
agent: outcome.agent,
|
|
119
|
+
outcome: outcome.outcome,
|
|
120
|
+
summary: String(summary).slice(0, 160),
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const projects = [...projectMap.values()]
|
|
125
|
+
.map((entry) => ({ ...entry, lastAt: entry.lastAt ? new Date(entry.lastAt).toISOString() : null }))
|
|
126
|
+
.sort((a, b) => b.tasks - a.tasks)
|
|
127
|
+
|
|
128
|
+
// --- reuse evidence ---
|
|
129
|
+
const reuseRecords = readJsonl(path.join(home, 'experience_reuse.jsonl')).filter((record) => {
|
|
130
|
+
if (!record || typeof record !== 'object') return false
|
|
131
|
+
if (project && record.project !== project) return false
|
|
132
|
+
const ts = Date.parse(record.timestamp || '')
|
|
133
|
+
return Number.isFinite(ts) && ts >= cutoff
|
|
134
|
+
})
|
|
135
|
+
const reuse = {
|
|
136
|
+
verified: reuseRecords.filter(isVerifiedReuse).length,
|
|
137
|
+
pendingAutoClaims: reuseRecords.filter(
|
|
138
|
+
(record) => record.source === 'auto_claim' && record.confirmed !== true
|
|
139
|
+
).length,
|
|
140
|
+
total: reuseRecords.length,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- rule-based suggestions (facts first, thresholds conservative) ---
|
|
144
|
+
const suggestions = []
|
|
145
|
+
const reliable = agents.filter((a) => a.total >= 5)
|
|
146
|
+
if (reliable.length >= 2) {
|
|
147
|
+
const best = reliable.reduce((a, b) => (b.successRate > a.successRate ? b : a))
|
|
148
|
+
const worst = reliable.reduce((a, b) => (b.successRate < a.successRate ? b : a))
|
|
149
|
+
if (best.successRate - worst.successRate >= 20) {
|
|
150
|
+
suggestions.push(
|
|
151
|
+
`路由信号: ${best.agent} 成功率 ${best.successRate}% vs ${worst.agent} ${worst.successRate}%(近 ${days} 天),同类任务优先路由给 ${best.agent}。`
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (errorHotspots.total >= 5 && errorHotspots.byAgent.length > 0) {
|
|
156
|
+
const top = errorHotspots.byAgent[0]
|
|
157
|
+
suggestions.push(
|
|
158
|
+
`错误热点: ${top.agent} 近 ${days} 天产生 ${top.count} 条 error trace${errorHotspots.byTool.length > 0 ? `(高发: ${errorHotspots.byTool[0].key} x${errorHotspots.byTool[0].count})` : ''},优先排查。`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
if (reuse.pendingAutoClaims >= 10) {
|
|
162
|
+
suggestions.push(
|
|
163
|
+
`待确认: ${reuse.pendingAutoClaims} 条 auto-claim 经验复用未确认,运行 mio.experience.confirm 提升证据质量。`
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const report = {
|
|
168
|
+
generatedAt: new Date(atTime).toISOString(),
|
|
169
|
+
periodDays: days,
|
|
170
|
+
project: project || 'all',
|
|
171
|
+
overview: {
|
|
172
|
+
tasks: outcomes.length,
|
|
173
|
+
byOutcome: outcomes.reduce((acc, t) => {
|
|
174
|
+
const oc = String(t.outcome || 'none').toLowerCase()
|
|
175
|
+
acc[oc] = (acc[oc] || 0) + 1
|
|
176
|
+
return acc
|
|
177
|
+
}, {}),
|
|
178
|
+
errorTraces: errors.length,
|
|
179
|
+
activeAgents: agents.length,
|
|
180
|
+
activeProjects: projects.length,
|
|
181
|
+
},
|
|
182
|
+
agents,
|
|
183
|
+
errorHotspots,
|
|
184
|
+
projects,
|
|
185
|
+
reuse,
|
|
186
|
+
suggestions,
|
|
187
|
+
}
|
|
188
|
+
report.markdown = renderMarkdown(report)
|
|
189
|
+
return report
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function renderMarkdown(report) {
|
|
193
|
+
const lines = []
|
|
194
|
+
const from = new Date(Date.parse(report.generatedAt) - report.periodDays * 86400000)
|
|
195
|
+
.toISOString()
|
|
196
|
+
.slice(0, 10)
|
|
197
|
+
const to = report.generatedAt.slice(0, 10)
|
|
198
|
+
lines.push(`# Mio Digest (${from} ~ ${to}, project=${report.project})`)
|
|
199
|
+
lines.push('')
|
|
200
|
+
const ov = report.overview
|
|
201
|
+
const rate = ov.tasks > 0 ? Math.round(((ov.byOutcome.success || 0) / ov.tasks) * 100) : 0
|
|
202
|
+
lines.push(
|
|
203
|
+
`- 任务 ${ov.tasks} 条(success ${ov.byOutcome.success || 0} / failure ${ov.byOutcome.failure || 0} / aborted ${ov.byOutcome.aborted || 0},成功率 ${rate}%),error trace ${ov.errorTraces} 条`
|
|
204
|
+
)
|
|
205
|
+
lines.push(`- 活跃 agent ${ov.activeAgents} 个,活跃项目 ${ov.activeProjects} 个`)
|
|
206
|
+
lines.push('')
|
|
207
|
+
|
|
208
|
+
if (report.agents.length > 0) {
|
|
209
|
+
lines.push('## Agent 表现')
|
|
210
|
+
lines.push('')
|
|
211
|
+
lines.push('| agent | 任务 | 成功 | 失败 | 中止 | 成功率 |')
|
|
212
|
+
lines.push('|---|---|---|---|---|---|')
|
|
213
|
+
for (const a of report.agents) {
|
|
214
|
+
lines.push(`| ${a.agent} | ${a.total} | ${a.success} | ${a.failure} | ${a.aborted} | ${a.successRate}% |`)
|
|
215
|
+
}
|
|
216
|
+
lines.push('')
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (report.projects.length > 0) {
|
|
220
|
+
lines.push('## 项目动态')
|
|
221
|
+
lines.push('')
|
|
222
|
+
for (const p of report.projects) {
|
|
223
|
+
const lastAt = p.lastAt ? p.lastAt.slice(0, 16).replace('T', ' ') : '-'
|
|
224
|
+
lines.push(`### ${p.project}(${p.tasks} 任务,最近 ${lastAt})`)
|
|
225
|
+
for (const task of p.recent) {
|
|
226
|
+
const at = task.at ? task.at.slice(5, 16).replace('T', ' ') : ''
|
|
227
|
+
lines.push(`- [${task.outcome}] (${at} ${task.agent}) ${task.summary}`)
|
|
228
|
+
}
|
|
229
|
+
lines.push('')
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (report.errorHotspots.total > 0) {
|
|
234
|
+
lines.push('## 错误热点')
|
|
235
|
+
lines.push('')
|
|
236
|
+
for (const item of report.errorHotspots.byAgent) {
|
|
237
|
+
lines.push(`- ${item.agent}: ${item.count} 条`)
|
|
238
|
+
}
|
|
239
|
+
const tools = report.errorHotspots.byTool.slice(0, 5)
|
|
240
|
+
if (tools.length > 0) {
|
|
241
|
+
lines.push(`- 高发位置: ${tools.map((t) => `${t.key} x${t.count}`).join(', ')}`)
|
|
242
|
+
}
|
|
243
|
+
lines.push('')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
lines.push('## 经验复用')
|
|
247
|
+
lines.push('')
|
|
248
|
+
lines.push(`- verified ${report.reuse.verified} / pending auto-claim ${report.reuse.pendingAutoClaims} / total ${report.reuse.total}`)
|
|
249
|
+
lines.push('')
|
|
250
|
+
|
|
251
|
+
if (report.suggestions.length > 0) {
|
|
252
|
+
lines.push('## 可行动建议')
|
|
253
|
+
lines.push('')
|
|
254
|
+
for (const suggestion of report.suggestions) {
|
|
255
|
+
lines.push(`- ${suggestion}`)
|
|
256
|
+
}
|
|
257
|
+
lines.push('')
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return lines.join('\n')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { generate, renderMarkdown }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = { createDigest }
|
|
@@ -5,6 +5,7 @@ const adapters = {
|
|
|
5
5
|
opencode: require('../adapters/opencode.js'),
|
|
6
6
|
workbuddy: require('../adapters/workbuddy.js'),
|
|
7
7
|
hermes: require('../adapters/hermes.js'),
|
|
8
|
+
claude: require('../adapters/claude-code.js'),
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
const HOST_CAPABILITIES = [
|
|
@@ -24,6 +25,10 @@ const HOST_CAPABILITIES = [
|
|
|
24
25
|
name: 'hermes',
|
|
25
26
|
capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'observer-transcript', 'policy-check', 'experience-reuse', 'runtime-status'],
|
|
26
27
|
},
|
|
28
|
+
{
|
|
29
|
+
name: 'claude',
|
|
30
|
+
capabilities: ['mcp-tools', 'memory', 'observer-ingest', 'observer-transcript', 'policy-check', 'experience-reuse', 'runtime-status'],
|
|
31
|
+
},
|
|
27
32
|
]
|
|
28
33
|
|
|
29
34
|
function safeInstalled(adapter) {
|