mio-agent-runtime 0.13.4 → 0.14.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 +44 -14
- 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 +223 -15
- package/observe/observer.js +1876 -1867
- package/package.json +5 -3
- package/scripts/link-workspace.cjs +52 -0
- package/server/creativity-engine.js +279 -114
- package/server/creativity-sources.js +186 -0
- package/server/evolution-cutover.js +5 -2
- package/server/idea-generate.js +136 -0
- 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 +103 -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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Source auto-aggregation for the creativity engine (spec Capability 2).
|
|
4
|
+
//
|
|
5
|
+
// Default providers read ONLY local MIO_HOME data — zero network. Insight and
|
|
6
|
+
// observer sources arrive as plain arrays from the caller, because only the
|
|
7
|
+
// entry point knows which store is authoritative (MIO_HOME for the CLI, the
|
|
8
|
+
// MCP server's MIO_DATA_DIR) and whether its optional package resolves.
|
|
9
|
+
// SourceAggregator already tolerates a throwing provider (per-provider
|
|
10
|
+
// try/catch with a WARN log), so providers here do not need extra guards.
|
|
11
|
+
|
|
12
|
+
const fs = require('fs')
|
|
13
|
+
const path = require('path')
|
|
14
|
+
const { SourceAggregator } = require('@akemi-mio/creativity/SourceAggregator')
|
|
15
|
+
const { sourcesFromInsights, isoOf } = require('./creativity-engine.js')
|
|
16
|
+
|
|
17
|
+
function readJsonl(file) {
|
|
18
|
+
try {
|
|
19
|
+
return fs
|
|
20
|
+
.readFileSync(file, 'utf8')
|
|
21
|
+
.split('\n')
|
|
22
|
+
.filter(Boolean)
|
|
23
|
+
.map((line) => {
|
|
24
|
+
try { return JSON.parse(line) } catch { return null }
|
|
25
|
+
})
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
} catch (_) {
|
|
28
|
+
return []
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Every weight is > 0.7 on purpose: IdeaGenerator.applyTemperature keeps any
|
|
33
|
+
// source with weight > 0.7 unconditionally (temperature 0.5 middle branch), so
|
|
34
|
+
// the assembled set reaches the prompt intact — deterministic, no RNG survival.
|
|
35
|
+
const WEIGHTS = {
|
|
36
|
+
memory: 0.75,
|
|
37
|
+
traceOk: 0.74,
|
|
38
|
+
traceFail: 0.73,
|
|
39
|
+
hypothesisActive: 0.72,
|
|
40
|
+
hypothesisRejected: 0.72,
|
|
41
|
+
trend: 0.76,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function memoryProvider(dataDir) {
|
|
45
|
+
return {
|
|
46
|
+
domain: 'feedback',
|
|
47
|
+
name: 'memory',
|
|
48
|
+
collect() {
|
|
49
|
+
return readJsonl(path.join(dataDir, 'memory.jsonl'))
|
|
50
|
+
.filter((r) => r && r.archived !== true && (r.kind === 'decision' || r.kind === 'note'))
|
|
51
|
+
.slice(-6)
|
|
52
|
+
.map((r) => ({
|
|
53
|
+
name: `${r.kind}: ${String(r.content || '').slice(0, 40)}`,
|
|
54
|
+
content: String(r.content || ''),
|
|
55
|
+
type: 'feedback',
|
|
56
|
+
weight: WEIGHTS.memory,
|
|
57
|
+
origin: `memory:${r.id}`,
|
|
58
|
+
originId: r.id,
|
|
59
|
+
timestamp: isoOf(r.timestamp),
|
|
60
|
+
}))
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function traceProviders(dataDir) {
|
|
66
|
+
const events = () => readJsonl(path.join(dataDir, 'traces.jsonl')).slice(-40)
|
|
67
|
+
const fmt = (r) => `${r.event_type}${r.outcome ? `/${r.outcome}` : ''}: ${JSON.stringify(r.payload || {}).slice(0, 300)}`
|
|
68
|
+
const stamp = (r) => isoOf(r.timestamp ?? r.ts)
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
domain: 'behavior',
|
|
72
|
+
name: 'traces-success',
|
|
73
|
+
collect() {
|
|
74
|
+
return events()
|
|
75
|
+
.filter((r) => r.event_type === 'task_outcome' && r.outcome === 'success')
|
|
76
|
+
.slice(-3)
|
|
77
|
+
.map((r) => ({
|
|
78
|
+
name: `success: ${String((r.payload && (r.payload.task || r.payload.summary)) || r.trace_id || 'task').slice(0, 40)}`,
|
|
79
|
+
content: fmt(r),
|
|
80
|
+
type: 'behavior',
|
|
81
|
+
weight: WEIGHTS.traceOk,
|
|
82
|
+
origin: `trace:${r.trace_id}`,
|
|
83
|
+
originId: r.trace_id,
|
|
84
|
+
timestamp: stamp(r),
|
|
85
|
+
}))
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
domain: 'failure',
|
|
90
|
+
name: 'traces-failure',
|
|
91
|
+
collect() {
|
|
92
|
+
return events()
|
|
93
|
+
.filter(
|
|
94
|
+
(r) =>
|
|
95
|
+
(r.event_type === 'error' || r.event_type === 'retry') ||
|
|
96
|
+
(r.event_type === 'task_outcome' && (r.outcome === 'failure' || r.outcome === 'error' || r.outcome === 'aborted')),
|
|
97
|
+
)
|
|
98
|
+
.slice(-3)
|
|
99
|
+
.map((r) => ({
|
|
100
|
+
name: `failure: ${String(r.event_type)}${r.trace_id ? ` ${String(r.trace_id).slice(0, 12)}` : ''}`,
|
|
101
|
+
content: fmt(r),
|
|
102
|
+
type: 'failure',
|
|
103
|
+
weight: WEIGHTS.traceFail,
|
|
104
|
+
origin: `trace:${r.trace_id || 'unknown'}`,
|
|
105
|
+
originId: r.trace_id,
|
|
106
|
+
timestamp: stamp(r),
|
|
107
|
+
}))
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hypothesesProvider(dataDir) {
|
|
114
|
+
return {
|
|
115
|
+
domain: 'module',
|
|
116
|
+
name: 'hypotheses',
|
|
117
|
+
collect() {
|
|
118
|
+
return readJsonl(path.join(dataDir, 'creativity', 'creativity-hypotheses.jsonl'))
|
|
119
|
+
.filter((h) => h && (h.status === 'active' || h.status === 'rejected'))
|
|
120
|
+
.slice(-6)
|
|
121
|
+
.map((h) => ({
|
|
122
|
+
name: String(h.title || h.id).slice(0, 60),
|
|
123
|
+
content: `idea: ${h.idea}\nrisk: ${h.risk || '-'}`,
|
|
124
|
+
type: h.status === 'rejected' ? 'failure' : 'knowledge',
|
|
125
|
+
weight: h.status === 'rejected' ? WEIGHTS.hypothesisRejected : WEIGHTS.hypothesisActive,
|
|
126
|
+
origin: `hypothesis:${h.id}`,
|
|
127
|
+
originId: h.id,
|
|
128
|
+
timestamp: isoOf(h.createdAt),
|
|
129
|
+
}))
|
|
130
|
+
},
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Observer trend reports → provocation sources. The report schema has drifted
|
|
135
|
+
// across observer versions, so fields are read defensively with fallbacks.
|
|
136
|
+
function trendSources(trends) {
|
|
137
|
+
return (Array.isArray(trends) ? trends : []).slice(0, 4).map((t, i) => {
|
|
138
|
+
const title = (t && (t.title || t.name || t.date)) || `trend-${i}`
|
|
139
|
+
const body =
|
|
140
|
+
(t && t.summary) ||
|
|
141
|
+
(Array.isArray(t && t.themes) ? t.themes.join('; ') : '') ||
|
|
142
|
+
JSON.stringify(t || {}).slice(0, 400)
|
|
143
|
+
const key = (t && (t.date || t.id)) || String(i)
|
|
144
|
+
const stamp = isoOf(t && (t.generatedAt || t.date))
|
|
145
|
+
return {
|
|
146
|
+
name: String(title).slice(0, 60),
|
|
147
|
+
content: String(body),
|
|
148
|
+
type: 'provocation',
|
|
149
|
+
weight: WEIGHTS.trend,
|
|
150
|
+
origin: `observer-trends:${key}`,
|
|
151
|
+
...(t && t.id ? { originId: t.id } : {}),
|
|
152
|
+
...(stamp ? { timestamp: stamp } : {}),
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Assemble every local (+ optionally insight/observer) domain through the
|
|
158
|
+
// package SourceAggregator so dedup, per-domain caps and failure tolerance
|
|
159
|
+
// behave identically for idea.generate and the generate top-up.
|
|
160
|
+
function buildAutoSources({ dataDir, insights = [], trends = [] }) {
|
|
161
|
+
const aggregator = new SourceAggregator(4, 1)
|
|
162
|
+
aggregator.addProvider(memoryProvider(dataDir))
|
|
163
|
+
for (const provider of traceProviders(dataDir)) aggregator.addProvider(provider)
|
|
164
|
+
aggregator.addProvider(hypothesesProvider(dataDir))
|
|
165
|
+
if (Array.isArray(insights) && insights.length > 0) {
|
|
166
|
+
aggregator.addProvider({ domain: 'observation', name: 'insight', collect: () => sourcesFromInsights(insights) })
|
|
167
|
+
}
|
|
168
|
+
const trendList = Array.isArray(trends) ? trends : []
|
|
169
|
+
if (trendList.length > 0) {
|
|
170
|
+
aggregator.addProvider({ domain: 'external', name: 'observer-trends', collect: () => trendSources(trendList) })
|
|
171
|
+
}
|
|
172
|
+
return aggregator.build()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Explicit --source entries always win: auto sources only top a set up to the
|
|
176
|
+
// two entries the concept mixer needs; they never replace or reorder explicit
|
|
177
|
+
// ones (spec Capability 2, injection point 2).
|
|
178
|
+
function topUpSources(explicit, auto) {
|
|
179
|
+
const list = Array.isArray(explicit) ? explicit.filter(Boolean) : []
|
|
180
|
+
if (list.length >= 2) return list
|
|
181
|
+
const seen = new Set(list.map((s) => s.name))
|
|
182
|
+
const extra = (Array.isArray(auto) ? auto : []).filter((s) => s && !seen.has(s.name))
|
|
183
|
+
return [...list, ...extra]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { buildAutoSources, topUpSources, trendSources, memoryProvider, traceProviders, hypothesesProvider }
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// `mio.idea.generate` — one-shot goal-driven idea pipeline (spec Capability 1).
|
|
4
|
+
// Shared by the CLI (`mio idea generate`) and the MCP server (tool
|
|
5
|
+
// `mio.idea.generate`): each entry point only assembles entry-specific inputs
|
|
6
|
+
// (which store is authoritative, where local data lives) and renders the
|
|
7
|
+
// response this module returns.
|
|
8
|
+
//
|
|
9
|
+
// Contract:
|
|
10
|
+
// - source order: goal → context → constraints → grounding → local auto,
|
|
11
|
+
// deduplicated by `origin` (fallback name), first occurrence wins — so a
|
|
12
|
+
// memory that arrives both as a grounding hit and through the local
|
|
13
|
+
// provider contributes exactly once, the grounding copy first;
|
|
14
|
+
// - pairing strategy: cross-type `explore` only when two types exist, else
|
|
15
|
+
// the `stable` fallback (an all-knowledge set explores into ZERO combos);
|
|
16
|
+
// - every candidate passes evaluateNovelty against stored hypotheses: a
|
|
17
|
+
// near-duplicate of a REJECTED one is persisted as rejected with a reason,
|
|
18
|
+
// a merely-similar recent one is down-scored; drafts AND rejects both land
|
|
19
|
+
// in the store so later runs can dedup against them;
|
|
20
|
+
// - ideas are never written to Mio memory — CreativityStore only.
|
|
21
|
+
|
|
22
|
+
const { IdeaGenerator } = require('@akemi-mio/creativity/IdeaGenerator')
|
|
23
|
+
const { evaluateNovelty } = require('@akemi-mio/creativity/NoveltyScorer')
|
|
24
|
+
|
|
25
|
+
// All > 0.7: IdeaGenerator.applyTemperature keeps such sources unconditionally
|
|
26
|
+
// (temperature 0.5 middle branch) — deterministic prompt, no RNG survival.
|
|
27
|
+
const WEIGHTS = { goal: 0.95, context: 0.9, constraint: 0.88, grounding: 0.75 }
|
|
28
|
+
|
|
29
|
+
// `queryMemory` result → grounding sources + ids. Tolerates null/empty
|
|
30
|
+
// (a thrown query is the caller's problem to catch; a miss is not).
|
|
31
|
+
function groundingFrom(queryResult) {
|
|
32
|
+
const results = Array.isArray(queryResult && queryResult.results) ? queryResult.results : []
|
|
33
|
+
const sources = results.map((r) => ({
|
|
34
|
+
name: `${r.kind || 'memory'}: ${String(r.content || '').slice(0, 40)}`,
|
|
35
|
+
content: String(r.content || ''),
|
|
36
|
+
type: 'feedback',
|
|
37
|
+
weight: WEIGHTS.grounding,
|
|
38
|
+
origin: `memory:${r.id}`,
|
|
39
|
+
originId: r.id,
|
|
40
|
+
...(r.timestamp ? { timestamp: r.timestamp } : {}),
|
|
41
|
+
}))
|
|
42
|
+
return { sources, relatedMemoryIds: results.map((r) => r.id), groundedWith: results.length }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function namedSource(role, text, origin, weight) {
|
|
46
|
+
const body = String(text).trim()
|
|
47
|
+
return {
|
|
48
|
+
name: `${role}: ${body.slice(0, 40)}`,
|
|
49
|
+
content: body,
|
|
50
|
+
type: 'knowledge',
|
|
51
|
+
weight,
|
|
52
|
+
origin,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runIdeaGenerate(options) {
|
|
57
|
+
const {
|
|
58
|
+
goal,
|
|
59
|
+
context = '',
|
|
60
|
+
constraints = [],
|
|
61
|
+
numIdeas,
|
|
62
|
+
chatJson,
|
|
63
|
+
store,
|
|
64
|
+
groundingSources = [],
|
|
65
|
+
relatedMemoryIds = [],
|
|
66
|
+
autoSources = [],
|
|
67
|
+
} = options || {}
|
|
68
|
+
|
|
69
|
+
const ordered = []
|
|
70
|
+
if (typeof goal === 'string' && goal.trim()) ordered.push(namedSource('goal', goal, 'goal', WEIGHTS.goal))
|
|
71
|
+
if (typeof context === 'string' && context.trim()) ordered.push(namedSource('context', context, 'context', WEIGHTS.context))
|
|
72
|
+
if (Array.isArray(constraints)) {
|
|
73
|
+
constraints.forEach((c, i) => {
|
|
74
|
+
if (typeof c === 'string' && c.trim()) ordered.push(namedSource('constraint', c, `constraint:${i}`, WEIGHTS.constraint))
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
for (const s of [...groundingSources, ...autoSources]) {
|
|
78
|
+
if (s && typeof s === 'object' && s.name) ordered.push(s)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const seen = new Set()
|
|
82
|
+
const sources = []
|
|
83
|
+
for (const s of ordered) {
|
|
84
|
+
const key = s.origin || s.name
|
|
85
|
+
if (seen.has(key)) continue
|
|
86
|
+
seen.add(key)
|
|
87
|
+
sources.push(s)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (sources.length < 2) {
|
|
91
|
+
return { reason: 'need at least 2 sources — add --context/--constraints, or run `mio remember` first' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const pairingStrategy = new Set(sources.map((s) => s.type)).size >= 2 ? 'explore' : 'stable'
|
|
95
|
+
const requested = Number(numIdeas)
|
|
96
|
+
const maxIdeas = Number.isFinite(requested) ? Math.min(Math.max(Math.trunc(requested), 1), 3) : 3
|
|
97
|
+
|
|
98
|
+
const rotationSeed = store.getHypotheses().length
|
|
99
|
+
const generator = new IdeaGenerator(chatJson, 0.5, undefined, rotationSeed)
|
|
100
|
+
// method left undefined: HypothesisGenerator rotates techniques internally
|
|
101
|
+
// from rotationSeed; the chosen one comes back on hypothesis.technique.
|
|
102
|
+
const creativeIdeas = await generator.generateIdeas(sources, maxIdeas, pairingStrategy, [])
|
|
103
|
+
|
|
104
|
+
const generatedAt = new Date().toISOString()
|
|
105
|
+
const recent = store.getHypotheses().slice(-30)
|
|
106
|
+
const rejected = store.getHypotheses({ status: 'rejected' }).slice(-50)
|
|
107
|
+
|
|
108
|
+
const ideas = []
|
|
109
|
+
const persistedIds = []
|
|
110
|
+
for (const { hypothesis: h, experiment } of creativeIdeas) {
|
|
111
|
+
const verdict = evaluateNovelty(h, recent, rejected)
|
|
112
|
+
if (verdict.shouldReject) {
|
|
113
|
+
h.status = 'rejected'
|
|
114
|
+
h.rejectionReason = verdict.rejectReason
|
|
115
|
+
} else {
|
|
116
|
+
h.novelty = verdict.adjustedNovelty
|
|
117
|
+
}
|
|
118
|
+
const labels = new Set(Array.isArray(h.sourceLabels) ? h.sourceLabels : [])
|
|
119
|
+
h.provenance = {
|
|
120
|
+
strategy: pairingStrategy,
|
|
121
|
+
technique: h.technique,
|
|
122
|
+
relatedMemoryIds: [...relatedMemoryIds],
|
|
123
|
+
sources: sources
|
|
124
|
+
.filter((s) => labels.has(s.name))
|
|
125
|
+
.map((s) => ({ name: s.name, type: s.type, origin: s.origin, timestamp: s.timestamp })),
|
|
126
|
+
generatedAt,
|
|
127
|
+
}
|
|
128
|
+
store.addHypothesis(h)
|
|
129
|
+
persistedIds.push(h.id)
|
|
130
|
+
ideas.push({ title: h.title, hypothesis: h, experiment, novelty: h.novelty, provenance: h.provenance })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { ideas, generatedAt, groundedWith: groundingSources.length, persistedIds }
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = { runIdeaGenerate, groundingFrom }
|
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) }
|