mio-agent-runtime 0.5.22 → 0.6.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.
@@ -0,0 +1,311 @@
1
+ 'use strict'
2
+
3
+ // Cross-agent evolution report (ADR-017 Evolution plane).
4
+ //
5
+ // Shared by the MCP tool `mio.evolution.report` and the CLI `mio evolution report`
6
+ // so the two cannot drift. Dependencies are injected -- the MCP server resolves
7
+ // its data directory from MIO_DATA_DIR while the CLI uses MIO_HOME, and each has
8
+ // its own projectName() -- so this module owns no path or environment policy.
9
+
10
+ const fs = require('fs')
11
+ const path = require('path')
12
+
13
+ const REPORT_FILES = {
14
+ memory: 'memory.jsonl',
15
+ trace: 'traces.jsonl',
16
+ reuse: 'experience_reuse.jsonl',
17
+ agent: 'agents.jsonl',
18
+ }
19
+
20
+ function readJsonl(file) {
21
+ if (!fs.existsSync(file)) return []
22
+ return fs
23
+ .readFileSync(file, 'utf8')
24
+ .split(/\r?\n/)
25
+ .filter(Boolean)
26
+ .map((line) => {
27
+ try {
28
+ return JSON.parse(line)
29
+ } catch (_) {
30
+ return null
31
+ }
32
+ })
33
+ .filter(Boolean)
34
+ }
35
+
36
+ const PERIOD_MS = {
37
+ '24h': 86400000,
38
+ '7d': 604800000,
39
+ '30d': 2592000000,
40
+ }
41
+
42
+ // `high` maps to 0, so the rank must be looked up with hasOwnProperty -- an
43
+ // `|| default` fallback would turn high (0, falsy) into the lowest priority and
44
+ // sort the most urgent suggestion last.
45
+ const SUGGESTION_RANK = { high: 0, medium: 1, low: 2 }
46
+
47
+ function suggestionRank(priority) {
48
+ return Object.prototype.hasOwnProperty.call(SUGGESTION_RANK, priority)
49
+ ? SUGGESTION_RANK[priority]
50
+ : SUGGESTION_RANK.low
51
+ }
52
+
53
+ function createEvolutionReport({ dataDir, projectName }) {
54
+ const files = {
55
+ memory: path.join(dataDir, REPORT_FILES.memory),
56
+ trace: path.join(dataDir, REPORT_FILES.trace),
57
+ reuse: path.join(dataDir, REPORT_FILES.reuse),
58
+ agent: path.join(dataDir, REPORT_FILES.agent),
59
+ }
60
+
61
+ function report(args = {}) {
62
+ const project = args.project || projectName()
63
+ const period = String(args.period || 'all').trim()
64
+ const now = Date.now()
65
+ const periodMs = PERIOD_MS[period] === undefined ? Infinity : PERIOD_MS[period]
66
+ const cutoff = periodMs === Infinity ? 0 : now - periodMs
67
+
68
+ const memories = readJsonl(files.memory).filter((m) => !project || m.project === project || !m.project)
69
+ const traces = readJsonl(files.trace).filter((t) => !project || t.project === project)
70
+ const reuses = readJsonl(files.reuse).filter((r) => !project || r.project === project)
71
+ const agents = readJsonl(files.agent).filter((a) => !project || a.project === project)
72
+
73
+ const filteredTraces = cutoff ? traces.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : traces
74
+ const filteredMemories = cutoff ? memories.filter((m) => new Date(m.timestamp).getTime() >= cutoff) : memories
75
+
76
+ // Ecosystem summary
77
+ const agentIds = new Set()
78
+ filteredTraces.forEach((t) => { if (t.agent) agentIds.add(t.agent) })
79
+ agents.forEach((a) => agentIds.add(a.agentId))
80
+ const outcomes = filteredTraces.filter((t) => t.event_type === 'task_outcome')
81
+ const successes = outcomes.filter((t) => t.outcome === 'success').length
82
+ const failures = outcomes.filter((t) => t.outcome === 'failure').length
83
+ const verifiedReuses = reuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
84
+ const pendingReuses = reuses.filter((r) => r.source === 'auto_claim' && !r.confirmed)
85
+
86
+ const ecosystem = {
87
+ agents: agentIds.size,
88
+ totalTasks: outcomes.length,
89
+ successRate: outcomes.length > 0 ? Math.round((successes / outcomes.length) * 100) : 0,
90
+ totalMemories: filteredMemories.length,
91
+ totalReuses: reuses.length,
92
+ verifiedReuses: verifiedReuses.length,
93
+ pendingReuses: pendingReuses.length,
94
+ }
95
+
96
+ // Agent performance comparison
97
+ const agentPerf = []
98
+ for (const agentId of agentIds) {
99
+ const agentTraces = filteredTraces.filter((t) => t.agent === agentId)
100
+ const agentOutcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
101
+ const agentSuccesses = agentOutcomes.filter((t) => t.outcome === 'success').length
102
+ const agentFailures = agentOutcomes.filter((t) => t.outcome === 'failure').length
103
+ const agentMemories = filteredMemories.filter((m) => m.source === agentId)
104
+ const agentVerified = reuses.filter((r) =>
105
+ (r.sourceAgent === agentId || r.targetAgent === agentId) && r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved
106
+ )
107
+ const reg = agents.find((a) => a.agentId === agentId)
108
+ agentPerf.push({
109
+ agentId,
110
+ tasks: {
111
+ total: agentOutcomes.length,
112
+ success: agentSuccesses,
113
+ failure: agentFailures,
114
+ successRate: agentOutcomes.length > 0 ? Math.round((agentSuccesses / agentOutcomes.length) * 100) : 0,
115
+ },
116
+ memories: agentMemories.length,
117
+ verifiedReuses: agentVerified.length,
118
+ registered: !!reg,
119
+ lastActive: reg ? reg.lastSeenAt : (agentTraces.length > 0 ? agentTraces[agentTraces.length - 1].timestamp : null),
120
+ })
121
+ }
122
+ agentPerf.sort((a, b) => b.tasks.successRate - a.tasks.successRate || b.tasks.total - a.tasks.total)
123
+
124
+ // Cross-agent patterns
125
+ const crossPatterns = []
126
+ const reuseByPair = {}
127
+ for (const r of reuses) {
128
+ const key = r.sourceAgent + '->' + r.targetAgent
129
+ if (!reuseByPair[key]) reuseByPair[key] = { source: r.sourceAgent, target: r.targetAgent, total: 0, verified: 0, improved: 0 }
130
+ reuseByPair[key].total++
131
+ if (r.confirmed) reuseByPair[key].verified++
132
+ if (r.outcomeImproved) reuseByPair[key].improved++
133
+ }
134
+ for (const [key, pair] of Object.entries(reuseByPair)) {
135
+ if (pair.source !== pair.target) {
136
+ crossPatterns.push({
137
+ type: 'reuse_path',
138
+ from: pair.source,
139
+ to: pair.target,
140
+ total: pair.total,
141
+ verified: pair.verified,
142
+ improved: pair.improved,
143
+ })
144
+ }
145
+ }
146
+ crossPatterns.sort((a, b) => b.verified - a.verified || b.total - a.total)
147
+
148
+ // Memory health
149
+ const byKind = {}
150
+ let shortContent = 0
151
+ let missingKind = 0
152
+ for (const m of filteredMemories) {
153
+ const kind = m.kind || 'unknown'
154
+ byKind[kind] = (byKind[kind] || 0) + 1
155
+ const content = String(m.content || '').trim()
156
+ if (content.length < 20) shortContent++
157
+ if (!m.kind) missingKind++
158
+ }
159
+ const memoryHealth = {
160
+ total: filteredMemories.length,
161
+ byKind,
162
+ shortContent,
163
+ missingKind,
164
+ qualityScore: filteredMemories.length > 0
165
+ ? Math.round(((filteredMemories.length - shortContent - missingKind) / filteredMemories.length) * 100)
166
+ : 100,
167
+ }
168
+
169
+ // Suggestions engine
170
+ const suggestions = []
171
+
172
+ // Routing suggestion: compare agent success rates
173
+ if (agentPerf.length >= 2) {
174
+ const best = agentPerf[0]
175
+ const worst = agentPerf[agentPerf.length - 1]
176
+ if (best.tasks.total >= 5 && worst.tasks.total >= 5 && best.tasks.successRate - worst.tasks.successRate >= 15) {
177
+ suggestions.push({
178
+ category: 'routing',
179
+ priority: 'high',
180
+ description: best.agentId + ' has ' + best.tasks.successRate + '% success rate vs ' + worst.agentId + ' at ' + worst.tasks.successRate + '%; prefer routing to ' + best.agentId,
181
+ basedOn: 'task_outcome comparison',
182
+ })
183
+ }
184
+ }
185
+
186
+ // Pending auto-claim suggestion
187
+ if (pendingReuses.length >= 10) {
188
+ suggestions.push({
189
+ category: 'evidence',
190
+ priority: 'medium',
191
+ description: pendingReuses.length + ' pending auto-claims awaiting confirmation; review cross-agent claims to increase verified reuse count',
192
+ basedOn: 'experience_reuse.jsonl',
193
+ })
194
+ }
195
+
196
+ // Memory quality suggestion
197
+ if (memoryHealth.qualityScore < 80 && filteredMemories.length > 10) {
198
+ suggestions.push({
199
+ category: 'memory',
200
+ priority: 'medium',
201
+ description: 'Memory quality score is ' + memoryHealth.qualityScore + '%; ' + shortContent + ' short records and ' + missingKind + ' missing kind fields found',
202
+ basedOn: 'memory.jsonl quality analysis',
203
+ })
204
+ }
205
+
206
+ // Cross-agent value suggestion
207
+ if (crossPatterns.length === 0 && agentIds.size >= 2) {
208
+ suggestions.push({
209
+ category: 'interop',
210
+ priority: 'high',
211
+ description: 'Multiple agents registered but no cross-agent reuse patterns detected; ensure agents query shared memory',
212
+ basedOn: 'experience_reuse.jsonl',
213
+ })
214
+ }
215
+
216
+ // Active agent suggestion
217
+ const inactiveAgents = agentPerf.filter((a) => {
218
+ if (!a.lastActive) return false
219
+ return (now - new Date(a.lastActive).getTime()) > 7 * 86400000
220
+ })
221
+ if (inactiveAgents.length > 0) {
222
+ suggestions.push({
223
+ category: 'coverage',
224
+ priority: 'low',
225
+ description: inactiveAgents.map((a) => a.agentId).join(', ') + ' inactive for >7 days; consider removal or reactivation',
226
+ basedOn: 'agent registry lastSeenAt',
227
+ })
228
+ }
229
+
230
+ suggestions.sort((a, b) => suggestionRank(a.priority) - suggestionRank(b.priority))
231
+
232
+ return {
233
+ project,
234
+ generatedAt: new Date().toISOString(),
235
+ period,
236
+ ecosystem,
237
+ agentPerformance: agentPerf,
238
+ crossAgentPatterns: crossPatterns,
239
+ memoryHealth,
240
+ suggestions,
241
+ }
242
+ }
243
+
244
+ return { report }
245
+ }
246
+
247
+ function formatEvolutionReportText(result) {
248
+ const lines = []
249
+ lines.push(`Evolution report -- project ${result.project || '(all)'}, period ${result.period}`)
250
+ lines.push(`Generated ${result.generatedAt}`)
251
+
252
+ const eco = result.ecosystem
253
+ lines.push('')
254
+ lines.push('Ecosystem')
255
+ lines.push(` agents ${eco.agents}`)
256
+ lines.push(` tasks ${eco.totalTasks}${eco.totalTasks > 0 ? ` (success rate ${eco.successRate}%)` : ''}`)
257
+ lines.push(` memories ${eco.totalMemories}`)
258
+ lines.push(` reuses ${eco.totalReuses} (verified ${eco.verifiedReuses}, pending ${eco.pendingReuses})`)
259
+
260
+ lines.push('')
261
+ lines.push('Agent performance')
262
+ if (result.agentPerformance.length === 0) {
263
+ lines.push(' (no agents seen in this window)')
264
+ } else {
265
+ for (const a of result.agentPerformance) {
266
+ const reg = a.registered ? 'registered' : 'unregistered'
267
+ lines.push(
268
+ ` ${a.agentId.padEnd(12)} tasks ${String(a.tasks.total).padStart(3)}` +
269
+ ` success ${String(a.tasks.successRate).padStart(3)}%` +
270
+ ` memories ${String(a.memories).padStart(3)}` +
271
+ ` verified reuses ${a.verifiedReuses} (${reg})`
272
+ )
273
+ }
274
+ }
275
+
276
+ lines.push('')
277
+ lines.push('Cross-agent patterns')
278
+ if (result.crossAgentPatterns.length === 0) {
279
+ lines.push(' (none)')
280
+ } else {
281
+ for (const p of result.crossAgentPatterns) {
282
+ lines.push(` ${p.from} -> ${p.to} total ${p.total} verified ${p.verified} improved ${p.improved}`)
283
+ }
284
+ }
285
+
286
+ const mh = result.memoryHealth
287
+ lines.push('')
288
+ lines.push('Memory health')
289
+ lines.push(` quality ${mh.qualityScore}% total ${mh.total} short ${mh.shortContent} missing kind ${mh.missingKind}`)
290
+ const kinds = Object.entries(mh.byKind)
291
+ if (kinds.length > 0) {
292
+ lines.push(` by kind: ${kinds.map(([k, n]) => `${k} ${n}`).join(', ')}`)
293
+ }
294
+
295
+ lines.push('')
296
+ lines.push('Suggestions')
297
+ if (result.suggestions.length === 0) {
298
+ lines.push(' (none)')
299
+ } else {
300
+ for (const s of result.suggestions) {
301
+ lines.push(` [${s.priority}] ${s.category}: ${s.description}`)
302
+ }
303
+ }
304
+
305
+ return lines.join('\n')
306
+ }
307
+
308
+ module.exports = {
309
+ createEvolutionReport,
310
+ formatEvolutionReportText,
311
+ }
@@ -0,0 +1,214 @@
1
+ 'use strict'
2
+
3
+ // Shared experience-reuse store: the single implementation behind
4
+ // mio.experience.list / confirm / reuse, consumed by both the MCP server and
5
+ // the CLI (`mio experience`). Kept separate from memory-store.js because it
6
+ // owns a different file (experience_reuse.jsonl) and a different concern —
7
+ // whether a past experience actually helped when reused, not recall ranking.
8
+ //
9
+ // The status vocabulary here is load-bearing: `verified` is the only status
10
+ // that feeds memory ranking and task routing, and an unconfirmed `auto_claim`
11
+ // never qualifies. That is why the CLI exposes pending claims explicitly —
12
+ // unconfirmed auto-claims are otherwise invisible dead weight.
13
+
14
+ const path = require('path')
15
+ const { createId, readJsonl, appendJsonl, writeJsonl } = require('./memory-store.js')
16
+
17
+ const REUSE_STATUS_FILTERS = Object.freeze({
18
+ pending: (record) => record.source === 'auto_claim' && record.confirmed !== true,
19
+ confirmed: (record) => record.confirmed === true,
20
+ verified: (record) =>
21
+ (record.reuse === true || record.reuse === 'true') &&
22
+ (record.behaviorChanged === true || record.behaviorChanged === 'true') &&
23
+ (record.outcomeImproved === true || record.outcomeImproved === 'true'),
24
+ auto_claim: (record) => record.source === 'auto_claim',
25
+ agent_report: (record) => record.source !== 'auto_claim',
26
+ })
27
+
28
+ const REUSE_STATUSES = Object.freeze(Object.keys(REUSE_STATUS_FILTERS))
29
+
30
+ function toBool(value) {
31
+ return value === true || value === 'true'
32
+ }
33
+
34
+ function visibleRecord(record) {
35
+ return {
36
+ id: record.id,
37
+ source: record.source,
38
+ sourceAgent: record.sourceAgent,
39
+ targetAgent: record.targetAgent,
40
+ experienceId: record.experienceId,
41
+ reuse: toBool(record.reuse),
42
+ behaviorChanged: toBool(record.behaviorChanged),
43
+ outcomeImproved: toBool(record.outcomeImproved),
44
+ confirmed: record.confirmed === true,
45
+ confirmedBy: record.confirmedBy || null,
46
+ traceId: record.traceId || null,
47
+ timestamp: record.timestamp,
48
+ project: record.project,
49
+ notes: typeof record.notes === 'string' && record.notes ? record.notes.slice(0, 300) : null,
50
+ }
51
+ }
52
+
53
+ function createExperienceStore(options = {}) {
54
+ const dataDir = options.dataDir
55
+ const projectName = options.projectName || (() => null)
56
+ // Optional hook returning a phase-0 summary for the affected project. The MCP
57
+ // server wires it so its response shape stays unchanged; the CLI may omit it.
58
+ const phase0Summary = options.phase0Summary || null
59
+ const experiencePath = path.join(dataDir, 'experience_reuse.jsonl')
60
+
61
+ function listReuse(args = {}) {
62
+ const project = args.project || projectName()
63
+ const status = String(args.status || 'all').trim().toLowerCase()
64
+ const targetAgent = String(args.targetAgent || '').trim().toLowerCase()
65
+ const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100)
66
+
67
+ let records = readJsonl(experiencePath)
68
+ if (project) records = records.filter((record) => record.project === project)
69
+ if (targetAgent) {
70
+ records = records.filter(
71
+ (record) => String(record.targetAgent || '').toLowerCase() === targetAgent
72
+ )
73
+ }
74
+ const matcher = REUSE_STATUS_FILTERS[status]
75
+ if (matcher) records = records.filter(matcher)
76
+ records.sort((a, b) => {
77
+ const at = new Date(a.timestamp || 0).getTime()
78
+ const bt = new Date(b.timestamp || 0).getTime()
79
+ return bt - at
80
+ })
81
+
82
+ const visible = records.slice(0, limit).map(visibleRecord)
83
+
84
+ return {
85
+ project: project || null,
86
+ status,
87
+ count: visible.length,
88
+ total: records.length,
89
+ records: visible,
90
+ }
91
+ }
92
+
93
+ // Confirms one record (legacy `id`) or many (`ids`).
94
+ //
95
+ // Single-id calls keep strict semantics: an unknown id or a non-auto_claim
96
+ // record throws, so a caller that named one record always learns when it was
97
+ // wrong. Bulk calls are best-effort — with dozens of pending auto-claims a
98
+ // single bad id must not abort the whole batch — and report `notFound` /
99
+ // `rejected` instead.
100
+ function confirmReuse(args = {}) {
101
+ const bulk = Array.isArray(args.ids)
102
+ const ids = (
103
+ bulk ? args.ids : args.id ? [args.id] : []
104
+ )
105
+ .map((value) => String(value).trim())
106
+ .filter(Boolean)
107
+
108
+ if (ids.length === 0) {
109
+ throw new Error('experience.confirm requires id of an auto-claimed reuse record')
110
+ }
111
+
112
+ const outcomeImprovedArg =
113
+ args.outcomeImproved === undefined || args.outcomeImproved === null
114
+ ? null
115
+ : toBool(args.outcomeImproved)
116
+ const confirmedBy = String(args.confirmedBy || 'agent').trim() || 'agent'
117
+ const notes = args.notes ? String(args.notes).trim() : null
118
+
119
+ const records = readJsonl(experiencePath)
120
+ const confirmedIds = []
121
+ const confirmedRecords = []
122
+ const notFound = []
123
+ const rejected = []
124
+ const alreadyConfirmed = []
125
+ const confirmedAt = new Date().toISOString()
126
+
127
+ for (const id of ids) {
128
+ const index = records.findIndex((record) => record && record.id === id)
129
+ if (index < 0) {
130
+ if (!bulk) throw new Error(`No experience reuse record found with id ${id}`)
131
+ notFound.push(id)
132
+ continue
133
+ }
134
+ const record = records[index]
135
+ if (record.source !== 'auto_claim') {
136
+ if (!bulk) throw new Error('experience.confirm only applies to source=auto_claim reuse records')
137
+ rejected.push(id)
138
+ continue
139
+ }
140
+ if (record.confirmed === true && !args.force) {
141
+ alreadyConfirmed.push(id)
142
+ continue
143
+ }
144
+ const updated = {
145
+ ...record,
146
+ behaviorChanged: true,
147
+ outcomeImproved: outcomeImprovedArg === null ? record.outcomeImproved === true : outcomeImprovedArg,
148
+ confirmed: true,
149
+ confirmedAt,
150
+ confirmedBy,
151
+ notes: notes || record.notes || null,
152
+ }
153
+ records[index] = updated
154
+ confirmedIds.push(id)
155
+ confirmedRecords.push(updated)
156
+ }
157
+
158
+ if (confirmedRecords.length > 0) writeJsonl(experiencePath, records)
159
+
160
+ const result = {
161
+ confirmed: confirmedRecords.length > 0,
162
+ // `evidence` stays a single record for back-compat with single-id callers
163
+ // (including the MCP tests); bulk callers should use confirmedIds.
164
+ evidence: confirmedRecords.length === 1 ? confirmedRecords[0] : confirmedRecords[0] || null,
165
+ confirmedCount: confirmedRecords.length,
166
+ confirmedIds,
167
+ notFound,
168
+ rejected,
169
+ alreadyConfirmed,
170
+ }
171
+ if (phase0Summary && confirmedRecords.length > 0) {
172
+ result.phase0 = phase0Summary(confirmedRecords[0].project)
173
+ }
174
+ return result
175
+ }
176
+
177
+ function recordReuse(args = {}) {
178
+ const sourceAgent = String(args.sourceAgent || '').trim()
179
+ const targetAgent = String(args.targetAgent || '').trim()
180
+ const experienceId = String(args.experienceId || '').trim()
181
+ if (!sourceAgent) throw new Error('experience.reuse requires sourceAgent')
182
+ if (!targetAgent) throw new Error('experience.reuse requires targetAgent')
183
+ if (!experienceId) throw new Error('experience.reuse requires experienceId')
184
+
185
+ const project = args.project || projectName()
186
+ const evidence = {
187
+ id: createId('xfer'),
188
+ timestamp: new Date().toISOString(),
189
+ sourceAgent,
190
+ targetAgent,
191
+ experienceId,
192
+ reuse: toBool(args.reuse),
193
+ behaviorChanged: toBool(args.behaviorChanged),
194
+ outcomeImproved: toBool(args.outcomeImproved),
195
+ project,
196
+ source: args.source || 'agent_report',
197
+ notes: args.notes || null,
198
+ }
199
+ appendJsonl(experiencePath, evidence)
200
+
201
+ const result = { recorded: true, evidence }
202
+ if (phase0Summary) result.phase0 = phase0Summary(project)
203
+ return result
204
+ }
205
+
206
+ return { experiencePath, listReuse, confirmReuse, recordReuse }
207
+ }
208
+
209
+ module.exports = {
210
+ createExperienceStore,
211
+ REUSE_STATUS_FILTERS,
212
+ REUSE_STATUSES,
213
+ visibleRecord,
214
+ }
@@ -0,0 +1,118 @@
1
+ 'use strict'
2
+
3
+ // Shared insight self-observation store: the single implementation behind the
4
+ // mio.insight.* MCP tools and the `mio insight` CLI command. Same rationale as
5
+ // memory-store.js / experience-store.js / policy-store.js -- one implementation,
6
+ // two entry points, so CLI and MCP cannot drift.
7
+ //
8
+ // @akemi-mio/insight is an *optional* dependency: every entry point throws the
9
+ // same "not installed" error, and isInsightAvailable() lets the MCP server gate
10
+ // tool registration on it.
11
+
12
+ const path = require('path')
13
+
14
+ let InsightStore = null
15
+ let InsightGenerator = null
16
+ try {
17
+ const insight = require('@akemi-mio/insight')
18
+ InsightStore = insight.InsightStore
19
+ InsightGenerator = insight.InsightGenerator
20
+ } catch {}
21
+
22
+ function isInsightAvailable() {
23
+ return Boolean(InsightStore)
24
+ }
25
+
26
+ // Score at or above which an unreported insight is counted as high-value in the
27
+ // status summary.
28
+ const INSIGHT_HIGH_VALUE_SCORE = 0.7
29
+ const INSIGHT_HIGH_VALUE_LIMIT = 50
30
+
31
+ function createInsightStore(options = {}) {
32
+ const dataDir = options.dataDir
33
+ const chatJson = options.chatJson
34
+ const storePath =
35
+ options.storePath || (dataDir ? path.join(dataDir, 'insights', 'insights.json') : null)
36
+
37
+ // Built on first use rather than at construction: the MCP server used to
38
+ // build it eagerly at module load, which touched the filesystem even when no
39
+ // insight tool was ever called.
40
+ let store = null
41
+ function requireStore() {
42
+ if (!InsightStore) throw new Error('@akemi-mio/insight not installed')
43
+ if (!storePath) throw new Error('insight store requires dataDir or storePath')
44
+ if (!store) store = new InsightStore(storePath)
45
+ return store
46
+ }
47
+
48
+ function status() {
49
+ const s = requireStore()
50
+ const all = s.getAll()
51
+ const unreported = s.getUnreported()
52
+ return {
53
+ total: all.length,
54
+ unreported: unreported.length,
55
+ reported: all.length - unreported.length,
56
+ highValue: s.getHighValueUnreported(INSIGHT_HIGH_VALUE_LIMIT, INSIGHT_HIGH_VALUE_SCORE).length,
57
+ }
58
+ }
59
+
60
+ function list(args = {}) {
61
+ const s = requireStore()
62
+ let insights = s.getAll()
63
+ if (args.unreported) insights = s.getUnreported()
64
+ if (args.minScore) insights = insights.filter((i) => i.score >= args.minScore)
65
+ if (args.detector) insights = insights.filter((i) => i.detector === args.detector)
66
+ if (args.limit) insights = insights.slice(-args.limit)
67
+ return insights
68
+ }
69
+
70
+ function markReported(args = {}) {
71
+ const s = requireStore()
72
+ if (!args.ids || !Array.isArray(args.ids)) throw new Error('ids array required')
73
+ for (const id of args.ids) s.markReported(id)
74
+ return { marked: args.ids.length }
75
+ }
76
+
77
+ async function generate(args = {}) {
78
+ if (!InsightGenerator) throw new Error('@akemi-mio/insight not installed')
79
+ const generator = new InsightGenerator({ chatJson })
80
+ const ctx = {
81
+ memoryEntries: (args.memories || []).map((m) => ({
82
+ type: m.kind || m.type || 'note',
83
+ content: m.content || '',
84
+ createdAt: m.createdAt || m.timestamp || Date.now(),
85
+ })),
86
+ summaries: args.summaries || [],
87
+ interactionCount: 0,
88
+ plans: (args.plans || []).map((p) => ({
89
+ title: p.title || p.name || '',
90
+ status: p.status || 'unknown',
91
+ updatedAt: p.updatedAt || Date.now(),
92
+ steps: p.steps || [],
93
+ createdAt: p.createdAt || Date.now(),
94
+ })),
95
+ eventCount: 0,
96
+ }
97
+ const insights = await generator.generate(ctx)
98
+ if (InsightStore && storePath) {
99
+ requireStore().addMany(insights)
100
+ }
101
+ return { generated: insights.length, insights }
102
+ }
103
+
104
+ return {
105
+ storePath,
106
+ status,
107
+ list,
108
+ markReported,
109
+ generate,
110
+ }
111
+ }
112
+
113
+ module.exports = {
114
+ createInsightStore,
115
+ isInsightAvailable,
116
+ INSIGHT_HIGH_VALUE_SCORE,
117
+ INSIGHT_HIGH_VALUE_LIMIT,
118
+ }