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.
- package/README.md +502 -77
- package/bin/mio.js +1929 -92
- package/observe/observer.js +100 -4
- package/package.json +45 -38
- package/scripts/check-syntax.cjs +72 -0
- package/scripts/verify-packed-runtime.js +23 -8
- package/server/agent-store.js +140 -0
- package/server/digest.js +12 -0
- package/server/evolution-report.js +311 -0
- package/server/experience-store.js +214 -0
- package/server/insight-store.js +118 -0
- package/server/llm-client.js +69 -0
- package/server/memory-store.js +505 -3
- package/server/mio-intelligence-mcp/index.js +127 -1466
- package/server/observer-store.js +219 -0
- package/server/policy-store.js +263 -0
- package/server/query-log.js +66 -0
- package/server/subscription-store.js +213 -0
- package/server/task-store.js +385 -0
|
@@ -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
|
+
}
|