mio-agent-runtime 0.13.4 → 0.13.5
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 +19 -5
- package/adapters/codex.js +121 -121
- package/adapters/hermes.js +182 -176
- package/adapters/opencode.js +203 -202
- package/adapters/workbuddy.js +197 -197
- package/bin/mio.js +93 -12
- package/observe/observer.js +1876 -1867
- package/package.json +3 -3
- package/server/creativity-engine.js +238 -67
- package/server/evolution-cutover.js +5 -2
- package/server/insight-store.js +37 -11
- package/server/llm-client.js +29 -8
- package/server/mio-intelligence-mcp/README.md +173 -173
- package/server/mio-intelligence-mcp/index.js +27 -6
- package/server/mio-intelligence-mcp/package.json +17 -17
- package/server/mio-intelligence-mcp/phase0.js +365 -365
- package/server/observer-store.js +12 -11
- package/server/task-store.js +475 -475
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.5",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"@akemi-mio/evolution-strategy": "^0.1.0",
|
|
34
34
|
"@akemi-mio/evolution-safety": "^0.1.0",
|
|
35
35
|
"@akemi-mio/evolution-scheduler": "^0.1.0",
|
|
36
|
-
"@akemi-mio/observer": "^0.1.
|
|
37
|
-
"@akemi-mio/insight": "^0.1.
|
|
36
|
+
"@akemi-mio/observer": "^0.1.4",
|
|
37
|
+
"@akemi-mio/insight": "^0.1.1"
|
|
38
38
|
},
|
|
39
39
|
"main": "index.js",
|
|
40
40
|
"directories": {
|
|
@@ -4,6 +4,29 @@ const fs = require('fs')
|
|
|
4
4
|
const path = require('path')
|
|
5
5
|
const crypto = require('crypto')
|
|
6
6
|
|
|
7
|
+
// Cache validity key for a JSONL file: a (size, mtimeMs) pair, the same
|
|
8
|
+
// guard server/insight-store.js uses (commit 0ba7fa8). Every getter below used
|
|
9
|
+
// to latch its parsed array forever, so a long-running MCP process answered
|
|
10
|
+
// `mio.creativity.list` from the copy it loaded at startup and a
|
|
11
|
+
// `mio creativity generate` in another process stayed invisible until restart.
|
|
12
|
+
function fileSignature(filePath) {
|
|
13
|
+
try {
|
|
14
|
+
const st = fs.statSync(filePath)
|
|
15
|
+
return `${st.size}:${st.mtimeMs}`
|
|
16
|
+
} catch (_) {
|
|
17
|
+
return 'missing'
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// One clamp for every LLM score: coerce, reject NaN/Infinity (a model that
|
|
22
|
+
// returns "high" used to store NaN and poison the sum gate), then bound to
|
|
23
|
+
// 0-100. Missing values fall back so old records keep a neutral score.
|
|
24
|
+
function clamp100(value, fallback = 50) {
|
|
25
|
+
const n = Number(value)
|
|
26
|
+
if (!Number.isFinite(n)) return fallback
|
|
27
|
+
return Math.min(100, Math.max(0, n))
|
|
28
|
+
}
|
|
29
|
+
|
|
7
30
|
// ═══════════════════════════════════════════════
|
|
8
31
|
// JSONL Store — ideas, combos, experiments
|
|
9
32
|
// ═══════════════════════════════════════════════
|
|
@@ -17,6 +40,9 @@ class CreativityStore {
|
|
|
17
40
|
this._hypotheses = null
|
|
18
41
|
this._combos = null
|
|
19
42
|
this._experiments = null
|
|
43
|
+
this._hypothesesSig = null
|
|
44
|
+
this._combosSig = null
|
|
45
|
+
this._experimentsSig = null
|
|
20
46
|
}
|
|
21
47
|
|
|
22
48
|
_ensureDir() {
|
|
@@ -42,8 +68,10 @@ class CreativityStore {
|
|
|
42
68
|
}
|
|
43
69
|
|
|
44
70
|
getHypotheses(opts = {}) {
|
|
45
|
-
|
|
71
|
+
const sig = fileSignature(this.hypothesesPath)
|
|
72
|
+
if (this._hypotheses === null || this._hypothesesSig !== sig) {
|
|
46
73
|
this._hypotheses = this._readJsonl(this.hypothesesPath)
|
|
74
|
+
this._hypothesesSig = sig
|
|
47
75
|
}
|
|
48
76
|
let list = this._hypotheses
|
|
49
77
|
if (opts.status) list = list.filter(h => h.status === opts.status)
|
|
@@ -53,6 +81,7 @@ class CreativityStore {
|
|
|
53
81
|
|
|
54
82
|
addHypothesis(h) {
|
|
55
83
|
this._appendJsonl(this.hypothesesPath, h)
|
|
84
|
+
this._hypothesesSig = fileSignature(this.hypothesesPath)
|
|
56
85
|
if (this._hypotheses) this._hypotheses.push(h)
|
|
57
86
|
}
|
|
58
87
|
|
|
@@ -63,30 +92,37 @@ class CreativityStore {
|
|
|
63
92
|
Object.assign(all[idx], patch)
|
|
64
93
|
this._hypotheses = all
|
|
65
94
|
this._rewriteFile(this.hypothesesPath, all)
|
|
95
|
+
this._hypothesesSig = fileSignature(this.hypothesesPath)
|
|
66
96
|
return true
|
|
67
97
|
}
|
|
68
98
|
|
|
69
99
|
getCombos(limit) {
|
|
70
|
-
|
|
100
|
+
const sig = fileSignature(this.combosPath)
|
|
101
|
+
if (this._combos === null || this._combosSig !== sig) {
|
|
71
102
|
this._combos = this._readJsonl(this.combosPath)
|
|
103
|
+
this._combosSig = sig
|
|
72
104
|
}
|
|
73
105
|
return limit ? this._combos.slice(-limit) : this._combos
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
addCombo(c) {
|
|
77
109
|
this._appendJsonl(this.combosPath, c)
|
|
110
|
+
this._combosSig = fileSignature(this.combosPath)
|
|
78
111
|
if (this._combos) this._combos.push(c)
|
|
79
112
|
}
|
|
80
113
|
|
|
81
114
|
getExperiments() {
|
|
82
|
-
|
|
115
|
+
const sig = fileSignature(this.experimentsPath)
|
|
116
|
+
if (this._experiments === null || this._experimentsSig !== sig) {
|
|
83
117
|
this._experiments = this._readJsonl(this.experimentsPath)
|
|
118
|
+
this._experimentsSig = sig
|
|
84
119
|
}
|
|
85
120
|
return this._experiments
|
|
86
121
|
}
|
|
87
122
|
|
|
88
123
|
addExperiment(e) {
|
|
89
124
|
this._appendJsonl(this.experimentsPath, e)
|
|
125
|
+
this._experimentsSig = fileSignature(this.experimentsPath)
|
|
90
126
|
if (this._experiments) this._experiments.push(e)
|
|
91
127
|
}
|
|
92
128
|
|
|
@@ -178,6 +214,30 @@ function evaluateNovelty(idea, recent, rejected) {
|
|
|
178
214
|
// Creativity Engine — main orchestrator
|
|
179
215
|
// ═══════════════════════════════════════════════
|
|
180
216
|
|
|
217
|
+
// Concept sources drawn from stored insights, backing
|
|
218
|
+
// `mio.creativity.generate { fromInsights: true }` / `mio creativity generate
|
|
219
|
+
// --from-insights`. The *caller* supplies the insight records because only the
|
|
220
|
+
// entry point knows which store is authoritative (MIO_HOME for the CLI, the
|
|
221
|
+
// MCP server's MIO_DATA_DIR); this only maps them onto the { name, content,
|
|
222
|
+
// type, weight } shape the concept mixer pairs up. Highest score first, capped
|
|
223
|
+
// so one busy insight run cannot turn into dozens of LLM calls.
|
|
224
|
+
function sourcesFromInsights(insights, limit = 10) {
|
|
225
|
+
return (Array.isArray(insights) ? insights : [])
|
|
226
|
+
.filter(i => i && (i.title || i.description || i.content))
|
|
227
|
+
.slice()
|
|
228
|
+
.sort((a, b) => (b.score || 0) - (a.score || 0))
|
|
229
|
+
.slice(0, limit)
|
|
230
|
+
.map(i => ({
|
|
231
|
+
name: String(i.title || i.id || 'insight').replace(/\s+/g, ' ').slice(0, 60),
|
|
232
|
+
content: [
|
|
233
|
+
i.description || i.content || i.title || '',
|
|
234
|
+
Array.isArray(i.evidence) && i.evidence.length > 0 ? `Evidence: ${i.evidence.join('; ')}` : '',
|
|
235
|
+
].filter(Boolean).join('\n'),
|
|
236
|
+
type: 'insight',
|
|
237
|
+
weight: 0.7,
|
|
238
|
+
}))
|
|
239
|
+
}
|
|
240
|
+
|
|
181
241
|
const STRATEGY_CYCLE = ['explore', 'signal', 'stable']
|
|
182
242
|
|
|
183
243
|
class CreativityEngine {
|
|
@@ -221,56 +281,84 @@ class CreativityEngine {
|
|
|
221
281
|
|
|
222
282
|
// Generate hypotheses via LLM
|
|
223
283
|
const ideas = []
|
|
284
|
+
// A failed pair used to vanish: `catch {}` swallowed the exception and a
|
|
285
|
+
// chatJson `{ error }` payload fell through the `if (result.data)` guard,
|
|
286
|
+
// so the caller saw `ideas: []` with no explanation of whether the LLM was
|
|
287
|
+
// down, the JSON was unparseable, or every pair really was a duplicate.
|
|
288
|
+
const errors = []
|
|
224
289
|
for (const [a, b] of pairs) {
|
|
225
290
|
const prompt = this._buildGenerationPrompt(a, b, strat, recent.slice(-10), rejected.slice(-5))
|
|
226
291
|
try {
|
|
227
292
|
const result = await this.chatJson(prompt, {
|
|
228
|
-
system:
|
|
293
|
+
system: [
|
|
294
|
+
'You are a creative hypothesis generator. Generate ONE novel hypothesis by combining the given concepts.',
|
|
295
|
+
'Reasoning rules:',
|
|
296
|
+
'1. First state the synergy mechanism: WHY these two concepts combine (what shared structure or complementary gap), not just THAT they pair.',
|
|
297
|
+
'2. Then give a causal chain: premise -> mechanism -> outcome. Each step must follow from the previous one; no unexplained jumps.',
|
|
298
|
+
'3. Be concrete: name the component, the change, and the observable effect. Vague claims ("improve efficiency", "be better") score 0 on logic.',
|
|
299
|
+
'4. State the strongest counter-risk honestly; an idea with no real risk is not being analyzed.',
|
|
300
|
+
'Output JSON with fields: title (<=60 chars), idea (the full causal chain, 100-250 words), expectedBenefit, risk, novelty (0-100), feasibility (0-100), impact (0-100), logic (0-100: how tight and complete the causal chain is).',
|
|
301
|
+
].join('\n'),
|
|
229
302
|
temperature: strat === 'explore' ? 0.8 : 0.4,
|
|
303
|
+
maxTokens: 900,
|
|
230
304
|
})
|
|
231
305
|
|
|
232
|
-
if (result.data) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
novelty: Math.min(100, Math.max(0, result.data.novelty || 50)),
|
|
240
|
-
feasibility: Math.min(100, Math.max(0, result.data.feasibility || 50)),
|
|
241
|
-
impact: Math.min(100, Math.max(0, result.data.impact || 50)),
|
|
242
|
-
sourceLabels: [a.name, b.name],
|
|
243
|
-
status: 'active',
|
|
244
|
-
createdAt: Date.now(),
|
|
245
|
-
strategy: strat,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Novelty check
|
|
249
|
-
const verdict = evaluateNovelty(h, recent, rejected)
|
|
250
|
-
if (verdict.shouldReject) {
|
|
251
|
-
h.status = 'rejected'
|
|
252
|
-
h.rejectionReason = verdict.rejectReason
|
|
253
|
-
} else {
|
|
254
|
-
h.novelty = verdict.adjustedNovelty
|
|
255
|
-
}
|
|
306
|
+
if (!result || !result.data) {
|
|
307
|
+
errors.push({
|
|
308
|
+
pair: [a.name, b.name],
|
|
309
|
+
error: (result && result.error) || 'LLM returned no data',
|
|
310
|
+
})
|
|
311
|
+
continue
|
|
312
|
+
}
|
|
256
313
|
|
|
257
|
-
|
|
258
|
-
|
|
314
|
+
const h = {
|
|
315
|
+
id: crypto.randomUUID(),
|
|
316
|
+
title: String(result.data.title || 'Untitled').slice(0, 60),
|
|
317
|
+
idea: result.data.idea || '',
|
|
318
|
+
expectedBenefit: result.data.expectedBenefit || '',
|
|
319
|
+
risk: result.data.risk || '',
|
|
320
|
+
novelty: clamp100(result.data.novelty || 50),
|
|
321
|
+
feasibility: clamp100(result.data.feasibility || 50),
|
|
322
|
+
impact: clamp100(result.data.impact || 50),
|
|
323
|
+
// logic is the fourth scoring dimension: a high-novelty idea with a
|
|
324
|
+
// broken causal chain must be visibly weaker than one that reasons
|
|
325
|
+
// cleanly. Defaults to 50 so records written before it existed keep
|
|
326
|
+
// a neutral value instead of NaN.
|
|
327
|
+
logic: clamp100(result.data.logic ?? 50),
|
|
328
|
+
sourceLabels: [a.name, b.name],
|
|
329
|
+
status: 'active',
|
|
330
|
+
createdAt: Date.now(),
|
|
331
|
+
strategy: strat,
|
|
332
|
+
}
|
|
259
333
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
334
|
+
// Novelty check
|
|
335
|
+
const verdict = evaluateNovelty(h, recent, rejected)
|
|
336
|
+
if (verdict.shouldReject) {
|
|
337
|
+
h.status = 'rejected'
|
|
338
|
+
h.rejectionReason = verdict.rejectReason
|
|
339
|
+
} else {
|
|
340
|
+
h.novelty = verdict.adjustedNovelty
|
|
267
341
|
}
|
|
342
|
+
|
|
343
|
+
this.store.addHypothesis(h)
|
|
344
|
+
ideas.push(h)
|
|
345
|
+
|
|
346
|
+
// Record combo
|
|
347
|
+
this.store.addCombo({
|
|
348
|
+
id: crypto.randomUUID(),
|
|
349
|
+
sources: [a.name, b.name],
|
|
350
|
+
description: h.title,
|
|
351
|
+
createdAt: Date.now(),
|
|
352
|
+
})
|
|
268
353
|
} catch (err) {
|
|
269
|
-
|
|
354
|
+
errors.push({
|
|
355
|
+
pair: [a.name, b.name],
|
|
356
|
+
error: String((err && err.message) || err),
|
|
357
|
+
})
|
|
270
358
|
}
|
|
271
359
|
}
|
|
272
360
|
|
|
273
|
-
return { ideas, strategy: strat, pairsAttempted: pairs.length }
|
|
361
|
+
return { ideas, strategy: strat, pairsAttempted: pairs.length, errors }
|
|
274
362
|
}
|
|
275
363
|
|
|
276
364
|
async ferment(limit = 5) {
|
|
@@ -283,48 +371,105 @@ class CreativityEngine {
|
|
|
283
371
|
}
|
|
284
372
|
|
|
285
373
|
const results = []
|
|
374
|
+
// Same reason as generate(): a failing review used to be indistinguishable
|
|
375
|
+
// from a hypothesis that simply had nothing new to say.
|
|
376
|
+
const errors = []
|
|
286
377
|
for (const h of fermentable) {
|
|
287
378
|
const prompt = `Review and refine this hypothesis. Current state:
|
|
288
379
|
Title: ${h.title}
|
|
289
380
|
Idea: ${h.idea}
|
|
290
|
-
Novelty: ${h.novelty}, Feasibility: ${h.feasibility}, Impact: ${h.impact}
|
|
381
|
+
Novelty: ${h.novelty}, Feasibility: ${h.feasibility}, Impact: ${h.impact}, Logic: ${h.logic ?? '(not scored)'}
|
|
291
382
|
|
|
292
|
-
|
|
383
|
+
Review checklist (apply in order):
|
|
384
|
+
1. Causal chain: does the idea explain WHY A leads to B at each step? If a step is asserted without a mechanism, lower logic and rewrite the step.
|
|
385
|
+
2. Evidence: is each claim traceable to the given idea text, or is it speculation presented as fact? Flag speculation.
|
|
386
|
+
3. Falsifiability: can you state how this idea would prove wrong? If not, say so in reason.
|
|
387
|
+
4. Concrete over vague: reject filler claims ("improve efficiency") by rewriting them with the specific component and effect.
|
|
388
|
+
|
|
389
|
+
Verdict rules:
|
|
390
|
+
- promote: the idea is mature AND the refined scores justify landing (novelty + feasibility + impact > 200 with logic >= 60).
|
|
391
|
+
- merge: only when this idea is clearly complementary to another stored one; set mergeWithId to that hypothesis id.
|
|
392
|
+
- reject: the causal chain cannot be repaired, or the premise lost its supporting signals.
|
|
393
|
+
- keep: everything else.
|
|
394
|
+
|
|
395
|
+
Output JSON with: title, idea, expectedBenefit, risk, novelty (0-100), feasibility (0-100), impact (0-100), logic (0-100), verdict (promote/keep/reject/merge), reason, mergeWithId (only for merge).`
|
|
293
396
|
|
|
294
397
|
try {
|
|
295
398
|
const result = await this.chatJson(prompt, {
|
|
296
|
-
system: 'You are a creative hypothesis reviewer.
|
|
399
|
+
system: 'You are a critical creative hypothesis reviewer. Reward tight causal reasoning, penalize vagueness. Output JSON only.',
|
|
297
400
|
temperature: 0.3,
|
|
401
|
+
maxTokens: 900,
|
|
298
402
|
})
|
|
299
403
|
|
|
300
|
-
if (result.data) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
404
|
+
if (!result || !result.data) {
|
|
405
|
+
errors.push({
|
|
406
|
+
id: h.id,
|
|
407
|
+
title: h.title,
|
|
408
|
+
error: (result && result.error) || 'LLM returned no data',
|
|
409
|
+
})
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const patch = {
|
|
414
|
+
title: String(result.data.title || h.title).slice(0, 60),
|
|
415
|
+
idea: result.data.idea || h.idea,
|
|
416
|
+
expectedBenefit: result.data.expectedBenefit || h.expectedBenefit,
|
|
417
|
+
risk: result.data.risk || h.risk,
|
|
418
|
+
novelty: clamp100(result.data.novelty || h.novelty),
|
|
419
|
+
feasibility: clamp100(result.data.feasibility || h.feasibility),
|
|
420
|
+
impact: clamp100(result.data.impact || h.impact),
|
|
421
|
+
logic: clamp100(result.data.logic ?? h.logic ?? 50),
|
|
422
|
+
fermentedAt: Date.now(),
|
|
423
|
+
fermentCount: (h.fermentCount || 0) + 1,
|
|
424
|
+
}
|
|
312
425
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
426
|
+
let verdict = result.data.verdict
|
|
427
|
+
if (verdict === 'reject') {
|
|
428
|
+
patch.status = 'rejected'
|
|
429
|
+
} else if (verdict === 'merge') {
|
|
430
|
+
// merge used to fall through silently: the verdict was echoed back
|
|
431
|
+
// in the results but nothing was merged. Now it behaves like
|
|
432
|
+
// IdeaFermentationEngine -- the reviewed idea is folded into the
|
|
433
|
+
// target and marked merged, or downgraded to keep when the target
|
|
434
|
+
// id is unknown (an unresolvable merge is not a merge).
|
|
435
|
+
const target = result.data.mergeWithId
|
|
436
|
+
? this.store.getHypotheses().find((x) => x.id === result.data.mergeWithId)
|
|
437
|
+
: null
|
|
438
|
+
if (target && target.id !== h.id) {
|
|
439
|
+
target.idea = [target.idea, patch.idea].filter(Boolean).join('\n---\n')
|
|
440
|
+
target.fermentedAt = patch.fermentedAt
|
|
441
|
+
target.fermentCount = (target.fermentCount || 0) + 1
|
|
442
|
+
this.store.updateHypothesis(target.id, {
|
|
443
|
+
idea: target.idea,
|
|
444
|
+
fermentedAt: target.fermentedAt,
|
|
445
|
+
fermentCount: target.fermentCount,
|
|
446
|
+
})
|
|
447
|
+
patch.status = 'merged'
|
|
448
|
+
patch.mergedInto = target.id
|
|
449
|
+
} else {
|
|
450
|
+
// An unresolvable merge target is not a merge; report it honestly
|
|
451
|
+
// as keep so the caller never sees a merge that did not happen.
|
|
452
|
+
verdict = 'keep'
|
|
317
453
|
}
|
|
454
|
+
}
|
|
318
455
|
|
|
319
|
-
|
|
320
|
-
|
|
456
|
+
// The gate is unconditional: patch.logic already folds in the
|
|
457
|
+
// review's score, the stored score, or 50. A reviewer that omits
|
|
458
|
+
// the field falls back to the stored/neutral value — omitting logic
|
|
459
|
+
// must not waive the gate it exists to enforce.
|
|
460
|
+
const logicOk = Number.isFinite(patch.logic) && patch.logic >= 60
|
|
461
|
+
if (verdict === 'promote' && (patch.novelty + patch.feasibility + patch.impact) > 200 && logicOk) {
|
|
462
|
+
patch.status = 'validated'
|
|
321
463
|
}
|
|
464
|
+
|
|
465
|
+
this.store.updateHypothesis(h.id, patch)
|
|
466
|
+
results.push({ id: h.id, title: h.title, verdict, reason: result.data.reason })
|
|
322
467
|
} catch (err) {
|
|
323
|
-
|
|
468
|
+
errors.push({ id: h.id, title: h.title, error: String((err && err.message) || err) })
|
|
324
469
|
}
|
|
325
470
|
}
|
|
326
471
|
|
|
327
|
-
return { fermented: results.length, results }
|
|
472
|
+
return { fermented: results.length, results, errors }
|
|
328
473
|
}
|
|
329
474
|
|
|
330
475
|
status() {
|
|
@@ -332,25 +477,36 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impa
|
|
|
332
477
|
const active = this.store.getHypotheses({ status: 'active' })
|
|
333
478
|
const validated = this.store.getHypotheses({ status: 'validated' })
|
|
334
479
|
const rejected = this.store.getHypotheses({ status: 'rejected' })
|
|
480
|
+
// draft is a real state -- ferment() reviews draft + active -- so hiding it
|
|
481
|
+
// here made `status` and `list --status draft` disagree.
|
|
482
|
+
const draft = this.store.getHypotheses({ status: 'draft' })
|
|
335
483
|
|
|
336
484
|
return {
|
|
337
485
|
...count,
|
|
338
486
|
active: active.length,
|
|
339
487
|
validated: validated.length,
|
|
340
488
|
rejected: rejected.length,
|
|
489
|
+
draft: draft.length,
|
|
341
490
|
recentIdeas: active.slice(-5).map(h => ({
|
|
342
491
|
id: h.id,
|
|
343
492
|
title: h.title,
|
|
344
493
|
novelty: h.novelty,
|
|
345
494
|
feasibility: h.feasibility,
|
|
346
495
|
impact: h.impact,
|
|
496
|
+
// Undefined keys vanish over JSON (the CLI path); keep them out here
|
|
497
|
+
// too so the engine and the CLI round-trip deep-equal.
|
|
498
|
+
...(h.logic !== undefined ? { logic: h.logic } : {}),
|
|
347
499
|
score: h.novelty + h.feasibility + h.impact,
|
|
348
500
|
})),
|
|
349
501
|
}
|
|
350
502
|
}
|
|
351
503
|
|
|
504
|
+
// The MCP schema and the CLI usage text both promise a default of 20; the
|
|
505
|
+
// implementation returned the whole store, so a large one flooded the tool
|
|
506
|
+
// result. `limit: 0` (and only 0) opts back into "everything".
|
|
352
507
|
list(opts = {}) {
|
|
353
|
-
|
|
508
|
+
const limit = opts.limit === undefined ? 20 : opts.limit
|
|
509
|
+
return this.store.getHypotheses({ ...opts, limit }).map(h => ({
|
|
354
510
|
id: h.id,
|
|
355
511
|
title: h.title,
|
|
356
512
|
idea: h.idea,
|
|
@@ -358,18 +514,24 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impa
|
|
|
358
514
|
novelty: h.novelty,
|
|
359
515
|
feasibility: h.feasibility,
|
|
360
516
|
impact: h.impact,
|
|
517
|
+
...(h.logic !== undefined ? { logic: h.logic } : {}),
|
|
361
518
|
score: h.novelty + h.feasibility + h.impact,
|
|
362
519
|
sourceLabels: h.sourceLabels,
|
|
363
520
|
createdAt: h.createdAt,
|
|
364
521
|
fermentedAt: h.fermentedAt,
|
|
365
522
|
fermentCount: h.fermentCount,
|
|
523
|
+
...(h.mergedInto !== undefined ? { mergedInto: h.mergedInto } : {}),
|
|
366
524
|
}))
|
|
367
525
|
}
|
|
368
526
|
|
|
369
527
|
_buildGenerationPrompt(a, b, strategy, recentHypotheses, rejectedHypotheses) {
|
|
528
|
+
// Long sources used to be pasted unbounded, crowding the instructions out
|
|
529
|
+
// of the context window; 600 chars keeps the pairing signal without the
|
|
530
|
+
// flood.
|
|
531
|
+
const clip = (s, n = 600) => (s.length > n ? s.slice(0, n) + '…' : s)
|
|
370
532
|
let prompt = `Combine these two concepts into ONE novel hypothesis:\n\n`
|
|
371
|
-
prompt += `Concept A: "${a.name}" — ${a.content}\n`
|
|
372
|
-
prompt += `Concept B: "${b.name}" — ${b.content}\n\n`
|
|
533
|
+
prompt += `Concept A: "${a.name}" — ${clip(a.content)}\n`
|
|
534
|
+
prompt += `Concept B: "${b.name}" — ${clip(b.content)}\n\n`
|
|
373
535
|
prompt += `Strategy: ${strategy}\n`
|
|
374
536
|
|
|
375
537
|
if (strategy === 'explore') {
|
|
@@ -380,6 +542,15 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impa
|
|
|
380
542
|
prompt += `Stable combination. Conservative refinement of existing ideas.\n`
|
|
381
543
|
}
|
|
382
544
|
|
|
545
|
+
// The logical-quality bar, stated where the model composes rather than
|
|
546
|
+
// only in the system prompt: synergy mechanism, explicit causal chain,
|
|
547
|
+
// and no claims that cannot name their mechanism.
|
|
548
|
+
prompt += `\nQuality requirements:\n`
|
|
549
|
+
prompt += `- The idea must explain the synergy mechanism: WHY these two concepts combine.\n`
|
|
550
|
+
prompt += `- Write the proposal as a causal chain (premise -> mechanism -> outcome), each step following from the last.\n`
|
|
551
|
+
prompt += `- Be specific: component, change, observable effect. No bare claims like "improve efficiency".\n`
|
|
552
|
+
prompt += `- The title must be self-contained (a reader sees only the title in lists).\n`
|
|
553
|
+
|
|
383
554
|
if (recentHypotheses.length > 0) {
|
|
384
555
|
prompt += `\nRecent ideas (avoid duplicates):\n`
|
|
385
556
|
for (const h of recentHypotheses) {
|
|
@@ -394,9 +565,9 @@ Output JSON with: title, idea, expectedBenefit, risk, novelty, feasibility, impa
|
|
|
394
565
|
}
|
|
395
566
|
}
|
|
396
567
|
|
|
397
|
-
prompt += `\nOutput JSON: {"title": "...", "idea": "...", "expectedBenefit": "...", "risk": "...", "novelty": 0-100, "feasibility": 0-100, "impact": 0-100}`
|
|
568
|
+
prompt += `\nOutput JSON: {"title": "...", "idea": "...", "expectedBenefit": "...", "risk": "...", "novelty": 0-100, "feasibility": 0-100, "impact": 0-100, "logic": 0-100}`
|
|
398
569
|
return prompt
|
|
399
570
|
}
|
|
400
571
|
}
|
|
401
572
|
|
|
402
|
-
module.exports = { CreativityEngine, CreativityStore }
|
|
573
|
+
module.exports = { CreativityEngine, CreativityStore, sourcesFromInsights }
|
|
@@ -72,13 +72,16 @@ function createEvolutionCutoverTools({ dataDir, appendJsonl, readJsonl, projectN
|
|
|
72
72
|
const project = projectFromArgs(args)
|
|
73
73
|
const shadowRuns = readJsonl(shadowPath).filter((record) => record.project === project)
|
|
74
74
|
const dualWriteRuns = readJsonl(dualWritePath).filter((record) => record.project === project)
|
|
75
|
+
// Apply documented defaults (schema says minShadowRuns: 5, maxMismatchRate: 0)
|
|
76
|
+
const minShadowRuns = Number.isFinite(args.minShadowRuns) ? args.minShadowRuns : 5
|
|
77
|
+
const maxMismatchRate = Number.isFinite(args.maxMismatchRate) ? args.maxMismatchRate : 0
|
|
75
78
|
return {
|
|
76
79
|
project,
|
|
77
80
|
...assessCutoverReadiness({
|
|
78
81
|
shadowRuns,
|
|
79
82
|
dualWriteRuns,
|
|
80
|
-
minShadowRuns
|
|
81
|
-
maxMismatchRate
|
|
83
|
+
minShadowRuns,
|
|
84
|
+
maxMismatchRate,
|
|
82
85
|
}),
|
|
83
86
|
}
|
|
84
87
|
}
|
package/server/insight-store.js
CHANGED
|
@@ -52,10 +52,12 @@ function isInsightAvailable() {
|
|
|
52
52
|
return Boolean(InsightStore)
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// "High value" means score >= 50 (0-100 scale) AND confidence >= 0.7 (0-1).
|
|
56
|
+
// These used to be named INSIGHT_HIGH_VALUE_SCORE = 0.7 / ..._LIMIT = 50 and
|
|
57
|
+
// were passed in (LIMIT, SCORE) order -- correct only because the constants
|
|
58
|
+
// happened to occupy each other's slots. Named for what they are now.
|
|
59
|
+
const INSIGHT_HIGH_VALUE_SCORE_THRESHOLD = 50
|
|
60
|
+
const INSIGHT_HIGH_VALUE_CONFIDENCE_THRESHOLD = 0.7
|
|
59
61
|
|
|
60
62
|
function createInsightStore(options = {}) {
|
|
61
63
|
const dataDir = options.dataDir
|
|
@@ -94,7 +96,10 @@ function createInsightStore(options = {}) {
|
|
|
94
96
|
total: all.length,
|
|
95
97
|
unreported: unreported.length,
|
|
96
98
|
reported: all.length - unreported.length,
|
|
97
|
-
highValue: s.getHighValueUnreported(
|
|
99
|
+
highValue: s.getHighValueUnreported(
|
|
100
|
+
INSIGHT_HIGH_VALUE_SCORE_THRESHOLD,
|
|
101
|
+
INSIGHT_HIGH_VALUE_CONFIDENCE_THRESHOLD,
|
|
102
|
+
).length,
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
105
|
|
|
@@ -125,7 +130,11 @@ function createInsightStore(options = {}) {
|
|
|
125
130
|
createdAt: m.createdAt || m.timestamp || Date.now(),
|
|
126
131
|
})),
|
|
127
132
|
summaries: args.summaries || [],
|
|
128
|
-
|
|
133
|
+
// 桌面端有真实交互流;CLI 默认没有。调用方可显式传入
|
|
134
|
+
// interactionCount/eventCount(如 MCP 侧按 trace 统计),
|
|
135
|
+
// 不传时 Scorer 会按记忆新鲜度给 confidence 兜底(旧逻辑
|
|
136
|
+
// 恒 0.2 会把 highValue 永久压成 0)。
|
|
137
|
+
interactionCount: Number(args.interactionCount) || 0,
|
|
129
138
|
plans: (args.plans || []).map((p) => ({
|
|
130
139
|
title: p.title || p.name || '',
|
|
131
140
|
status: p.status || 'unknown',
|
|
@@ -133,13 +142,30 @@ function createInsightStore(options = {}) {
|
|
|
133
142
|
steps: p.steps || [],
|
|
134
143
|
createdAt: p.createdAt || Date.now(),
|
|
135
144
|
})),
|
|
136
|
-
eventCount: 0,
|
|
145
|
+
eventCount: Number(args.eventCount) || 0,
|
|
137
146
|
}
|
|
138
147
|
const insights = await generator.generate(ctx)
|
|
139
148
|
if (InsightStore && storePath) {
|
|
140
|
-
|
|
149
|
+
// addMany is a pure push: every `mio insight generate` run used to
|
|
150
|
+
// append another copy of the same detection. Drop titles the store
|
|
151
|
+
// already holds (reported or not) so re-running is idempotent.
|
|
152
|
+
const s = requireStore()
|
|
153
|
+
const seen = new Set(s.getAll().map((i) => String(i.title || '').trim().toLowerCase()))
|
|
154
|
+
// Dedup against the store AND within the batch -- a generator that emits
|
|
155
|
+
// the same detection twice in one run must not survive just because the
|
|
156
|
+
// second copy was not there when the set was built.
|
|
157
|
+
const fresh = []
|
|
158
|
+
for (const i of insights || []) {
|
|
159
|
+
if (!i) continue
|
|
160
|
+
const key = i.title ? String(i.title).trim().toLowerCase() : null
|
|
161
|
+
if (key !== null && seen.has(key)) continue
|
|
162
|
+
if (key !== null) seen.add(key)
|
|
163
|
+
fresh.push(i)
|
|
164
|
+
}
|
|
165
|
+
if (fresh.length > 0) s.addMany(fresh)
|
|
166
|
+
return { generated: fresh.length, duplicates: (insights || []).length - fresh.length, insights: fresh }
|
|
141
167
|
}
|
|
142
|
-
return { generated: insights.length, insights }
|
|
168
|
+
return { generated: (insights || []).length, insights }
|
|
143
169
|
}
|
|
144
170
|
|
|
145
171
|
return {
|
|
@@ -154,6 +180,6 @@ function createInsightStore(options = {}) {
|
|
|
154
180
|
module.exports = {
|
|
155
181
|
createInsightStore,
|
|
156
182
|
isInsightAvailable,
|
|
157
|
-
|
|
158
|
-
|
|
183
|
+
INSIGHT_HIGH_VALUE_SCORE_THRESHOLD,
|
|
184
|
+
INSIGHT_HIGH_VALUE_CONFIDENCE_THRESHOLD,
|
|
159
185
|
}
|
package/server/llm-client.js
CHANGED
|
@@ -27,14 +27,13 @@ const path = require('path')
|
|
|
27
27
|
const DEFAULT_API_URL = 'https://opencode.ai/zen/go/v1/chat/completions'
|
|
28
28
|
const DEFAULT_MODEL = 'deepseek-v4-flash'
|
|
29
29
|
|
|
30
|
-
// Resolve the directory holding config.json. The CLI
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
30
|
+
// Resolve the directory holding config.json. The CLI uses MIO_HOME
|
|
31
|
+
// (~/.mio-intelligence by default). The MCP server uses MIO_DATA_DIR ||
|
|
32
|
+
// cwd/.mio-intelligence. This module serves the CLI-facing `llm-config`/
|
|
33
|
+
// `config llm` commands, so we follow the CLI convention: MIO_HOME first.
|
|
34
34
|
function homeDir() {
|
|
35
35
|
return (
|
|
36
36
|
process.env.MIO_HOME ||
|
|
37
|
-
process.env.MIO_DATA_DIR ||
|
|
38
37
|
path.join(os.homedir(), '.mio-intelligence')
|
|
39
38
|
)
|
|
40
39
|
}
|
|
@@ -118,6 +117,13 @@ async function chatJson(userText, opts = {}) {
|
|
|
118
117
|
const system = opts.system || 'You are a creative AI assistant. Output JSON.'
|
|
119
118
|
const temperature = opts.temperature ?? 0.3
|
|
120
119
|
|
|
120
|
+
// response_format must match the prompt's top-level shape. Prompts that
|
|
121
|
+
// demand a top-level JSON array (InsightPrompt) can be rejected outright
|
|
122
|
+
// (HTTP 400) by OpenAI-compatible gateways that enforce json_object, so
|
|
123
|
+
// array callers omit the field and rely on prompt + fence-stripping parse.
|
|
124
|
+
const wantsArray = opts.responseFormat === 'array'
|
|
125
|
+
const responseFormat = wantsArray ? undefined : { type: 'json_object' }
|
|
126
|
+
|
|
121
127
|
try {
|
|
122
128
|
const resp = await fetch(apiUrl, {
|
|
123
129
|
method: 'POST',
|
|
@@ -128,11 +134,12 @@ async function chatJson(userText, opts = {}) {
|
|
|
128
134
|
body: JSON.stringify({
|
|
129
135
|
model,
|
|
130
136
|
temperature,
|
|
137
|
+
...(opts.maxTokens ? { max_tokens: opts.maxTokens } : {}),
|
|
131
138
|
messages: [
|
|
132
139
|
{ role: 'system', content: system },
|
|
133
140
|
{ role: 'user', content: userText },
|
|
134
141
|
],
|
|
135
|
-
|
|
142
|
+
...(responseFormat ? { response_format: responseFormat } : {}),
|
|
136
143
|
}),
|
|
137
144
|
signal: AbortSignal.timeout(opts.timeoutMs || 30000),
|
|
138
145
|
})
|
|
@@ -140,10 +147,24 @@ async function chatJson(userText, opts = {}) {
|
|
|
140
147
|
if (!resp.ok) return { error: `HTTP ${resp.status}` }
|
|
141
148
|
const json = await resp.json()
|
|
142
149
|
const text = json.choices?.[0]?.message?.content || ''
|
|
150
|
+
|
|
151
|
+
// Parse failures used to return { data: rawText }, so a prose answer
|
|
152
|
+
// masqueraded as a parsed object: callers testing truthy `result.data`
|
|
153
|
+
// stored an untitled, score-less record (and polluted the novelty
|
|
154
|
+
// dedup pool). A model that answered with anything but JSON is an
|
|
155
|
+
// error, not a degraded success. One retry with a code fence stripped
|
|
156
|
+
// covers models that wrap JSON despite response_format.
|
|
143
157
|
try {
|
|
144
158
|
return { data: JSON.parse(text) }
|
|
145
|
-
} catch {
|
|
146
|
-
|
|
159
|
+
} catch (_) {}
|
|
160
|
+
const stripped = text.replace(/^\s*```(?:json)?\s*/i, '').replace(/\s*```\s*$/, '')
|
|
161
|
+
try {
|
|
162
|
+
return { data: JSON.parse(stripped) }
|
|
163
|
+
} catch (_) {
|
|
164
|
+
return {
|
|
165
|
+
error: 'model returned unparseable JSON',
|
|
166
|
+
raw: String(text).slice(0, 300),
|
|
167
|
+
}
|
|
147
168
|
}
|
|
148
169
|
} catch (err) {
|
|
149
170
|
return { error: String(err) }
|