mio-agent-runtime 0.3.5 → 0.3.7
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 +12 -3
- package/adapters/hermes.js +90 -0
- package/bin/mio.js +253 -247
- package/observe/observer.js +237 -0
- package/package.json +4 -3
- package/server/mio-intelligence-mcp/README.md +174 -0
- package/server/mio-intelligence-mcp/index.js +1179 -27
- package/server/mio-intelligence-mcp/package.json +18 -0
- package/server/mio-intelligence-mcp/phase0.js +381 -0
|
@@ -16,8 +16,13 @@ const dataDir = path.resolve(
|
|
|
16
16
|
const memoryPath = path.join(dataDir, 'memory.jsonl')
|
|
17
17
|
const tracePath = path.join(dataDir, 'traces.jsonl')
|
|
18
18
|
const experienceReusePath = path.join(dataDir, 'experience_reuse.jsonl')
|
|
19
|
+
const queryPath = path.join(dataDir, 'queries.jsonl')
|
|
20
|
+
const subscriptionPath = path.join(dataDir, 'subscriptions.jsonl')
|
|
21
|
+
const digestStatePath = path.join(dataDir, 'digest_state.json')
|
|
19
22
|
fs.mkdirSync(dataDir, { recursive: true })
|
|
20
23
|
|
|
24
|
+
const { loadPhase0, summarizePhase0, renderPhase0Markdown } = require('./phase0')
|
|
25
|
+
|
|
21
26
|
let runtimeContext = {}
|
|
22
27
|
try {
|
|
23
28
|
const rawContext = process.env.MIO_CONTEXT
|
|
@@ -35,6 +40,44 @@ function runtimeAgentId() {
|
|
|
35
40
|
return runtimeContext.agentId || null
|
|
36
41
|
}
|
|
37
42
|
|
|
43
|
+
// Phase 0 auto-claim: match memory.query results with a later task_outcome
|
|
44
|
+
// from the same agent+project, so reuse evidence does not depend on the
|
|
45
|
+
// agent self-reporting mio.experience.reuse.
|
|
46
|
+
const REUSE_MATCH_WINDOW_MS = (() => {
|
|
47
|
+
const minutes = Number(process.env.MIO_REUSE_MATCH_WINDOW_MIN)
|
|
48
|
+
return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : 60 * 60 * 1000
|
|
49
|
+
})()
|
|
50
|
+
const MAX_RECENT_QUERIES = 200
|
|
51
|
+
let recentQueries = []
|
|
52
|
+
|
|
53
|
+
function loadRecentQueries() {
|
|
54
|
+
const now = Date.now()
|
|
55
|
+
const entries = readJsonl(queryPath)
|
|
56
|
+
.filter(
|
|
57
|
+
(entry) =>
|
|
58
|
+
entry &&
|
|
59
|
+
typeof entry.agent === 'string' &&
|
|
60
|
+
entry.agent &&
|
|
61
|
+
typeof entry.expiresAt === 'number' &&
|
|
62
|
+
entry.expiresAt > now
|
|
63
|
+
)
|
|
64
|
+
.slice(-MAX_RECENT_QUERIES)
|
|
65
|
+
recentQueries = entries
|
|
66
|
+
return entries
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function persistRecentQueries(entries) {
|
|
70
|
+
const limited = entries.slice(-MAX_RECENT_QUERIES)
|
|
71
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
72
|
+
fs.writeFileSync(
|
|
73
|
+
queryPath,
|
|
74
|
+
limited.length > 0 ? limited.map((entry) => JSON.stringify(entry)).join('\n') + '\n' : '',
|
|
75
|
+
'utf8'
|
|
76
|
+
)
|
|
77
|
+
recentQueries = limited
|
|
78
|
+
return limited
|
|
79
|
+
}
|
|
80
|
+
|
|
38
81
|
function createId(prefix) {
|
|
39
82
|
return `${prefix}_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
|
|
40
83
|
}
|
|
@@ -43,6 +86,15 @@ function appendJsonl(file, value) {
|
|
|
43
86
|
fs.appendFileSync(file, `${JSON.stringify(value)}\n`, 'utf8')
|
|
44
87
|
}
|
|
45
88
|
|
|
89
|
+
function writeJsonl(file, values) {
|
|
90
|
+
fs.mkdirSync(path.dirname(file), { recursive: true })
|
|
91
|
+
fs.writeFileSync(
|
|
92
|
+
file,
|
|
93
|
+
values.length > 0 ? values.map((value) => JSON.stringify(value)).join('\n') + '\n' : '',
|
|
94
|
+
'utf8'
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
46
98
|
function readJsonl(file) {
|
|
47
99
|
if (!fs.existsSync(file)) return []
|
|
48
100
|
return fs
|
|
@@ -115,36 +167,158 @@ function tokenize(text) {
|
|
|
115
167
|
return [...latin, ...cjk]
|
|
116
168
|
}
|
|
117
169
|
|
|
118
|
-
function
|
|
170
|
+
function normalizeScope(value) {
|
|
171
|
+
const s = String(value || 'project').trim().toLowerCase()
|
|
172
|
+
if (s === 'global' || s === 'all') return s
|
|
173
|
+
return 'project'
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isGlobalRecord(record) {
|
|
177
|
+
return String(record.scope || '').trim().toLowerCase() === 'global'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function matchesProjectScope(record, project, scope) {
|
|
181
|
+
const s = normalizeScope(scope)
|
|
182
|
+
if (s === 'global') return isGlobalRecord(record)
|
|
183
|
+
if (s === 'all') {
|
|
184
|
+
if (isGlobalRecord(record)) return true
|
|
185
|
+
return !project || record.project === project
|
|
186
|
+
}
|
|
187
|
+
// project (default): project-scoped records matching the project only
|
|
188
|
+
if (isGlobalRecord(record)) return false
|
|
189
|
+
return !project || record.project === project
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function latinTokens(text) {
|
|
193
|
+
return String(text || '').toLowerCase().match(/[a-z0-9]+/g) || []
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function cjkBigrams(text) {
|
|
197
|
+
const chars = String(text || '').toLowerCase().match(/[\u4e00-\u9fff]/g) || []
|
|
198
|
+
const bigrams = new Set()
|
|
199
|
+
for (let i = 0; i + 1 < chars.length; i += 1) {
|
|
200
|
+
bigrams.add(`${chars[i]}${chars[i + 1]}`)
|
|
201
|
+
}
|
|
202
|
+
return bigrams
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function scoreRecord(record, queryText, project, evidence) {
|
|
119
206
|
if (project && record.project && record.project !== project) return 0
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
)
|
|
207
|
+
const content = `${record.content || ''} ${(record.tags || []).join(' ')} ${record.kind || ''}`
|
|
208
|
+
const haystackTokens = tokenize(content)
|
|
209
|
+
const haystackLatin = latinTokens(content)
|
|
210
|
+
const queryTokens = tokenize(queryText)
|
|
123
211
|
let score = 0
|
|
124
212
|
for (const token of queryTokens) {
|
|
125
|
-
if (
|
|
213
|
+
if (haystackTokens.includes(token)) score += 2
|
|
214
|
+
}
|
|
215
|
+
const queryLatin = queryTokens.filter((token) => !token.startsWith('cjk:'))
|
|
216
|
+
for (const token of queryLatin) {
|
|
217
|
+
if (
|
|
218
|
+
token.length >= 3 &&
|
|
219
|
+
haystackLatin.some((item) => item.startsWith(token) || token.startsWith(item))
|
|
220
|
+
) {
|
|
221
|
+
score += 1
|
|
222
|
+
}
|
|
126
223
|
}
|
|
127
|
-
|
|
224
|
+
const queryBigrams = cjkBigrams(queryText)
|
|
225
|
+
const haystackBigrams = cjkBigrams(content)
|
|
226
|
+
if (queryBigrams.size > 0 && haystackBigrams.size > 0) {
|
|
227
|
+
let matched = 0
|
|
228
|
+
for (const bigram of queryBigrams) {
|
|
229
|
+
if (haystackBigrams.has(bigram)) matched += 1
|
|
230
|
+
}
|
|
231
|
+
score += matched * 1.5
|
|
232
|
+
}
|
|
233
|
+
// Recency only boosts ranking among actual matches; it must not enable recall.
|
|
234
|
+
if (score > 0 && record.timestamp) {
|
|
128
235
|
const ageDays = Math.max(0, (Date.now() - Date.parse(record.timestamp)) / 86400000)
|
|
129
236
|
score += Math.max(0, 0.5 - ageDays * 0.05)
|
|
130
237
|
}
|
|
238
|
+
// Evidence weighting (P2): memories successfully reused rank higher; never enables recall.
|
|
239
|
+
if (score > 0 && evidence && evidence.has(record.id)) {
|
|
240
|
+
const ev = evidence.get(record.id)
|
|
241
|
+
score += Math.min(2, ev.reuseCount * 0.6 + (ev.confirmedCount > 0 ? 1 : 0))
|
|
242
|
+
}
|
|
131
243
|
return score
|
|
132
244
|
}
|
|
133
245
|
|
|
246
|
+
function matchesMemoryFilters(record, kind, tags) {
|
|
247
|
+
if (kind) {
|
|
248
|
+
const recordKind = String(record.kind || '').trim().toLowerCase()
|
|
249
|
+
if (recordKind !== kind) return false
|
|
250
|
+
}
|
|
251
|
+
if (tags.length > 0) {
|
|
252
|
+
const recordTags = new Set(
|
|
253
|
+
(record.tags || []).map((tag) => String(tag).trim().toLowerCase()),
|
|
254
|
+
)
|
|
255
|
+
for (const tag of tags) {
|
|
256
|
+
if (!recordTags.has(tag)) return false
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return true
|
|
260
|
+
}
|
|
261
|
+
|
|
134
262
|
function queryMemory(args = {}) {
|
|
135
263
|
const query = String(args.query || '')
|
|
136
264
|
const project = args.project || projectName()
|
|
137
265
|
const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
|
|
138
|
-
const
|
|
266
|
+
const kind = args.kind ? String(args.kind).trim().toLowerCase() : null
|
|
267
|
+
const tags = Array.isArray(args.tags)
|
|
268
|
+
? args.tags.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean)
|
|
269
|
+
: []
|
|
270
|
+
const scope = normalizeScope(args.scope)
|
|
271
|
+
const evidence = loadEvidenceWeights()
|
|
272
|
+
const scoreFor = (record) =>
|
|
273
|
+
scoreRecord(record, query, project, evidence) +
|
|
274
|
+
(scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
|
|
139
275
|
const results = readJsonl(memoryPath)
|
|
140
|
-
.filter((record) =>
|
|
276
|
+
.filter((record) => record.archived !== true)
|
|
277
|
+
.filter((record) => matchesMemoryFilters(record, kind, tags))
|
|
278
|
+
.filter((record) => matchesProjectScope(record, project, scope))
|
|
279
|
+
.filter((record) => scoreFor(record) > 0)
|
|
141
280
|
.sort((a, b) => {
|
|
142
|
-
return
|
|
281
|
+
return scoreFor(b) - scoreFor(a)
|
|
143
282
|
})
|
|
144
283
|
.slice(0, limit)
|
|
284
|
+
.map((record) => {
|
|
285
|
+
const ev = evidence.get(record.id)
|
|
286
|
+
if (!ev) return record
|
|
287
|
+
return {
|
|
288
|
+
...record,
|
|
289
|
+
evidence: {
|
|
290
|
+
reuseCount: ev.reuseCount,
|
|
291
|
+
confirmedCount: ev.confirmedCount,
|
|
292
|
+
lastReusedAt: ev.lastReusedAt,
|
|
293
|
+
},
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
const layerCounts = { project: 0, global: 0 }
|
|
297
|
+
for (const record of results) {
|
|
298
|
+
if (record.scope === 'global' || !record.project) layerCounts.global += 1
|
|
299
|
+
else layerCounts.project += 1
|
|
300
|
+
}
|
|
301
|
+
if (results.length > 0) {
|
|
302
|
+
const timestamp = Date.now()
|
|
303
|
+
const entries = loadRecentQueries()
|
|
304
|
+
entries.push({
|
|
305
|
+
agent: runtimeAgentId() || 'mcp',
|
|
306
|
+
project,
|
|
307
|
+
query,
|
|
308
|
+
resultIds: results.map((record) => record.id).filter(Boolean),
|
|
309
|
+
resultSources: results.map((record) => record.source || null),
|
|
310
|
+
timestamp,
|
|
311
|
+
expiresAt: timestamp + REUSE_MATCH_WINDOW_MS,
|
|
312
|
+
})
|
|
313
|
+
persistRecentQueries(entries)
|
|
314
|
+
}
|
|
145
315
|
return {
|
|
146
316
|
query,
|
|
147
317
|
project,
|
|
318
|
+
scope,
|
|
319
|
+
layers: layerCounts,
|
|
320
|
+
kind: kind || null,
|
|
321
|
+
tags: tags,
|
|
148
322
|
count: results.length,
|
|
149
323
|
results,
|
|
150
324
|
}
|
|
@@ -153,13 +327,15 @@ function queryMemory(args = {}) {
|
|
|
153
327
|
function recordMemory(args = {}) {
|
|
154
328
|
const content = String(args.content || '').trim()
|
|
155
329
|
if (!content) throw new Error('memory.record requires a non-empty content')
|
|
330
|
+
const scope = normalizeScope(args.scope)
|
|
156
331
|
const record = {
|
|
157
332
|
id: createId('mem'),
|
|
158
333
|
timestamp: new Date().toISOString(),
|
|
159
334
|
kind: args.kind || 'note',
|
|
160
335
|
content,
|
|
161
336
|
tags: normalizeTags(args.tags),
|
|
162
|
-
project: args.project || projectName(),
|
|
337
|
+
project: scope === 'global' ? null : args.project || projectName(),
|
|
338
|
+
scope,
|
|
163
339
|
source: args.source || runtimeAgentId() || 'mcp',
|
|
164
340
|
}
|
|
165
341
|
appendJsonl(memoryPath, record)
|
|
@@ -183,9 +359,543 @@ function ingestObservation(args = {}) {
|
|
|
183
359
|
project: args.project || projectName(),
|
|
184
360
|
}
|
|
185
361
|
appendJsonl(tracePath, event)
|
|
362
|
+
const autoClaims = autoClaimExperienceReuse(event)
|
|
363
|
+
if (autoClaims.length > 0) {
|
|
364
|
+
return { recorded: true, event, autoClaims }
|
|
365
|
+
}
|
|
186
366
|
return { recorded: true, event }
|
|
187
367
|
}
|
|
188
368
|
|
|
369
|
+
function autoClaimExperienceReuse(event) {
|
|
370
|
+
if (String(event.event_type || '').toLowerCase() !== 'task_outcome') return []
|
|
371
|
+
const targetAgent = String(event.agent || '').trim()
|
|
372
|
+
if (!targetAgent) return []
|
|
373
|
+
const now = Date.now()
|
|
374
|
+
const claims = []
|
|
375
|
+
const remaining = []
|
|
376
|
+
const entries = loadRecentQueries()
|
|
377
|
+
for (const entry of entries) {
|
|
378
|
+
const expired = now - entry.timestamp > REUSE_MATCH_WINDOW_MS
|
|
379
|
+
const matches =
|
|
380
|
+
entry.agent === targetAgent && (!event.project || entry.project === event.project)
|
|
381
|
+
if (expired || matches) {
|
|
382
|
+
if (matches && !expired) {
|
|
383
|
+
claims.push(buildAutoClaim(entry, event, targetAgent))
|
|
384
|
+
}
|
|
385
|
+
continue
|
|
386
|
+
}
|
|
387
|
+
remaining.push(entry)
|
|
388
|
+
}
|
|
389
|
+
persistRecentQueries(remaining)
|
|
390
|
+
for (const evidence of claims) {
|
|
391
|
+
appendJsonl(experienceReusePath, evidence)
|
|
392
|
+
}
|
|
393
|
+
return claims
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function buildAutoClaim(entry, event, targetAgent) {
|
|
397
|
+
const crossSourceIndex = entry.resultSources.findIndex(
|
|
398
|
+
(source) => source && String(source).toLowerCase() !== targetAgent.toLowerCase()
|
|
399
|
+
)
|
|
400
|
+
const sourceAgent =
|
|
401
|
+
crossSourceIndex >= 0
|
|
402
|
+
? String(entry.resultSources[crossSourceIndex])
|
|
403
|
+
: targetAgent
|
|
404
|
+
const experienceId =
|
|
405
|
+
entry.resultIds[crossSourceIndex >= 0 ? crossSourceIndex : 0] ||
|
|
406
|
+
entry.resultIds[0] ||
|
|
407
|
+
'unknown'
|
|
408
|
+
const outcomeImproved = String(event.outcome || '').toLowerCase() === 'success'
|
|
409
|
+
return {
|
|
410
|
+
id: createId('xfer'),
|
|
411
|
+
timestamp: new Date().toISOString(),
|
|
412
|
+
sourceAgent,
|
|
413
|
+
targetAgent,
|
|
414
|
+
experienceId,
|
|
415
|
+
reuse: true,
|
|
416
|
+
behaviorChanged: false,
|
|
417
|
+
outcomeImproved,
|
|
418
|
+
project: entry.project,
|
|
419
|
+
source: 'auto_claim',
|
|
420
|
+
traceId: event.trace_id || null,
|
|
421
|
+
notes: `Auto-claimed: memory.query "${entry.query}" matched task_outcome ${event.outcome || 'unknown'} in trace ${event.trace_id || 'unknown'}.`,
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const SUBSCRIPTION_DEFAULT_TTL_DAYS = 30
|
|
426
|
+
const MAX_SUBSCRIPTIONS_PER_AGENT = 20
|
|
427
|
+
const MAX_DIGEST_EVENTS = 100
|
|
428
|
+
|
|
429
|
+
function readDigestState() {
|
|
430
|
+
if (!fs.existsSync(digestStatePath)) return {}
|
|
431
|
+
try {
|
|
432
|
+
const parsed = JSON.parse(fs.readFileSync(digestStatePath, 'utf8'))
|
|
433
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
|
434
|
+
} catch (_) {
|
|
435
|
+
return {}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function writeDigestState(state) {
|
|
440
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
441
|
+
fs.writeFileSync(digestStatePath, JSON.stringify(state), 'utf8')
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function subscribeKey(agent, project, eventTypes, topic) {
|
|
445
|
+
return `${agent}|${project}|${eventTypes.join(',')}|${topic || ''}`
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function loadActiveSubscriptions(agent) {
|
|
449
|
+
const now = Date.now()
|
|
450
|
+
return readJsonl(subscriptionPath)
|
|
451
|
+
.filter((subscription) => subscription && subscription.agent === agent)
|
|
452
|
+
.filter((subscription) => {
|
|
453
|
+
if (subscription.expiresAt) {
|
|
454
|
+
const expires = new Date(subscription.expiresAt).getTime()
|
|
455
|
+
if (Number.isFinite(expires) && expires <= now) return false
|
|
456
|
+
}
|
|
457
|
+
return true
|
|
458
|
+
})
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function subscribeObserver(args = {}) {
|
|
462
|
+
const agent = runtimeAgentId() || 'mcp'
|
|
463
|
+
const project = args.project || projectName()
|
|
464
|
+
const eventTypes = Array.isArray(args.eventTypes)
|
|
465
|
+
? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
|
|
466
|
+
: []
|
|
467
|
+
const topic = args.topic ? String(args.topic).trim().slice(0, 120) : ''
|
|
468
|
+
const ttlDays = Number(args.ttlDays) || SUBSCRIPTION_DEFAULT_TTL_DAYS
|
|
469
|
+
const ttlMs = Math.max(1, ttlDays) * 24 * 60 * 60 * 1000
|
|
470
|
+
|
|
471
|
+
const subscriptions = readJsonl(subscriptionPath)
|
|
472
|
+
const key = subscribeKey(agent, project, eventTypes, topic)
|
|
473
|
+
let existing = subscriptions.find((subscription) => {
|
|
474
|
+
return (
|
|
475
|
+
subscription &&
|
|
476
|
+
subscribeKey(
|
|
477
|
+
subscription.agent,
|
|
478
|
+
subscription.project,
|
|
479
|
+
subscription.eventTypes || [],
|
|
480
|
+
subscription.topic || '',
|
|
481
|
+
) === key
|
|
482
|
+
)
|
|
483
|
+
})
|
|
484
|
+
const now = new Date()
|
|
485
|
+
const expiresAt = new Date(now.getTime() + ttlMs).toISOString()
|
|
486
|
+
if (existing) {
|
|
487
|
+
existing.updatedAt = now.toISOString()
|
|
488
|
+
existing.expiresAt = expiresAt
|
|
489
|
+
} else {
|
|
490
|
+
existing = {
|
|
491
|
+
id: createId('sub'),
|
|
492
|
+
agent,
|
|
493
|
+
project,
|
|
494
|
+
eventTypes,
|
|
495
|
+
topic: topic || null,
|
|
496
|
+
createdAt: now.toISOString(),
|
|
497
|
+
updatedAt: now.toISOString(),
|
|
498
|
+
expiresAt,
|
|
499
|
+
}
|
|
500
|
+
subscriptions.push(existing)
|
|
501
|
+
}
|
|
502
|
+
const agentSubscriptions = subscriptions.filter((subscription) => subscription.agent === agent)
|
|
503
|
+
if (agentSubscriptions.length > MAX_SUBSCRIPTIONS_PER_AGENT) {
|
|
504
|
+
throw new Error(`Too many subscriptions for agent ${agent} (max ${MAX_SUBSCRIPTIONS_PER_AGENT})`)
|
|
505
|
+
}
|
|
506
|
+
writeJsonl(subscriptionPath, subscriptions)
|
|
507
|
+
return { subscribed: true, subscription: existing, project }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function observerDigest(args = {}) {
|
|
511
|
+
const agent = runtimeAgentId() || 'mcp'
|
|
512
|
+
const project = args.project || projectName()
|
|
513
|
+
const limit = Math.min(Math.max(Number(args.limit) || 20, 1), MAX_DIGEST_EVENTS)
|
|
514
|
+
const filterEventTypes = Array.isArray(args.eventTypes)
|
|
515
|
+
? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
|
|
516
|
+
: null
|
|
517
|
+
|
|
518
|
+
let subscriptions = loadActiveSubscriptions(agent)
|
|
519
|
+
if (args.project) {
|
|
520
|
+
subscriptions = subscriptions.filter((subscription) => subscription.project === project)
|
|
521
|
+
}
|
|
522
|
+
if (filterEventTypes) {
|
|
523
|
+
subscriptions = subscriptions.filter((subscription) => {
|
|
524
|
+
if ((subscription.eventTypes || []).length === 0) return true
|
|
525
|
+
return filterEventTypes.some((eventType) =>
|
|
526
|
+
(subscription.eventTypes || []).includes(eventType),
|
|
527
|
+
)
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
const state = readDigestState()
|
|
532
|
+
const traces = readJsonl(tracePath)
|
|
533
|
+
const digestTime = new Date().toISOString()
|
|
534
|
+
const events = []
|
|
535
|
+
const seenIds = new Set()
|
|
536
|
+
const matchedSubscriptionIds = new Set()
|
|
537
|
+
|
|
538
|
+
for (const subscription of subscriptions) {
|
|
539
|
+
const cursor = state[subscription.id] || null
|
|
540
|
+
const cursorTime = cursor ? Date.parse(cursor) : null
|
|
541
|
+
const subscriptionEventTypes = subscription.eventTypes || []
|
|
542
|
+
let lastEventTime = null
|
|
543
|
+
for (const event of traces) {
|
|
544
|
+
if (subscription.project && event.project && event.project !== subscription.project) {
|
|
545
|
+
continue
|
|
546
|
+
}
|
|
547
|
+
if (
|
|
548
|
+
subscriptionEventTypes.length > 0 &&
|
|
549
|
+
!subscriptionEventTypes.includes(String(event.event_type || '').toLowerCase())
|
|
550
|
+
) {
|
|
551
|
+
continue
|
|
552
|
+
}
|
|
553
|
+
if (subscription.topic) {
|
|
554
|
+
const haystack = `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
|
|
555
|
+
.toLowerCase()
|
|
556
|
+
if (!haystack.includes(subscription.topic.toLowerCase())) continue
|
|
557
|
+
}
|
|
558
|
+
const eventTime = event.timestamp ? Date.parse(event.timestamp) : null
|
|
559
|
+
if (cursorTime !== null && eventTime !== null && eventTime <= cursorTime) continue
|
|
560
|
+
if (eventTime !== null && (lastEventTime === null || eventTime > lastEventTime)) {
|
|
561
|
+
lastEventTime = eventTime
|
|
562
|
+
}
|
|
563
|
+
if (seenIds.has(event.id)) continue
|
|
564
|
+
seenIds.add(event.id)
|
|
565
|
+
events.push({
|
|
566
|
+
id: event.id,
|
|
567
|
+
trace_id: event.trace_id || null,
|
|
568
|
+
event_type: event.event_type,
|
|
569
|
+
outcome: event.outcome || null,
|
|
570
|
+
timestamp: event.timestamp,
|
|
571
|
+
agent: event.agent || null,
|
|
572
|
+
payload: event.payload || null,
|
|
573
|
+
})
|
|
574
|
+
matchedSubscriptionIds.add(subscription.id)
|
|
575
|
+
if (events.length >= limit) break
|
|
576
|
+
}
|
|
577
|
+
if (lastEventTime !== null) {
|
|
578
|
+
state[subscription.id] = new Date(lastEventTime).toISOString()
|
|
579
|
+
} else if (cursor === null) {
|
|
580
|
+
state[subscription.id] = digestTime
|
|
581
|
+
}
|
|
582
|
+
if (events.length >= limit) break
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
writeDigestState(state)
|
|
586
|
+
return {
|
|
587
|
+
project,
|
|
588
|
+
agent,
|
|
589
|
+
subscriptionCount: subscriptions.length,
|
|
590
|
+
matchedSubscriptions: Array.from(matchedSubscriptionIds),
|
|
591
|
+
count: events.length,
|
|
592
|
+
events,
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function contentTokens(value) {
|
|
597
|
+
const text = String(value || '').toLowerCase()
|
|
598
|
+
const latin = text.match(/[a-z0-9]+/g) || []
|
|
599
|
+
const chars = text.match(/[\u4e00-\u9fff]/g) || []
|
|
600
|
+
const bigrams = []
|
|
601
|
+
for (let i = 0; i < chars.length - 1; i++) {
|
|
602
|
+
bigrams.push(`${chars[i]}${chars[i + 1]}`)
|
|
603
|
+
}
|
|
604
|
+
return [...latin, ...bigrams]
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function jaccardSimilarity(a, b) {
|
|
608
|
+
const setA = new Set(contentTokens(a))
|
|
609
|
+
const setB = new Set(contentTokens(b))
|
|
610
|
+
if (setA.size === 0 || setB.size === 0) return 0
|
|
611
|
+
let intersection = 0
|
|
612
|
+
for (const token of setA) {
|
|
613
|
+
if (setB.has(token)) intersection += 1
|
|
614
|
+
}
|
|
615
|
+
const union = setA.size + setB.size - intersection
|
|
616
|
+
return union === 0 ? 0 : intersection / union
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function findDuplicateGroups(records, limit) {
|
|
620
|
+
const parent = new Map()
|
|
621
|
+
for (const record of records) parent.set(record.id, record.id)
|
|
622
|
+
const find = (id) => {
|
|
623
|
+
while (parent.get(id) !== id) {
|
|
624
|
+
parent.set(id, parent.get(parent.get(id)))
|
|
625
|
+
id = parent.get(id)
|
|
626
|
+
}
|
|
627
|
+
return id
|
|
628
|
+
}
|
|
629
|
+
const union = (a, b) => {
|
|
630
|
+
const ra = find(a)
|
|
631
|
+
const rb = find(b)
|
|
632
|
+
if (ra !== rb) parent.set(ra, rb)
|
|
633
|
+
}
|
|
634
|
+
for (let i = 0; i < records.length; i++) {
|
|
635
|
+
for (let j = i + 1; j < records.length; j++) {
|
|
636
|
+
if (
|
|
637
|
+
jaccardSimilarity(records[i].content, records[j].content) >= MEMORY_DUPLICATE_SIMILARITY
|
|
638
|
+
) {
|
|
639
|
+
union(records[i].id, records[j].id)
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const byRoot = new Map()
|
|
644
|
+
for (const record of records) {
|
|
645
|
+
const root = find(record.id)
|
|
646
|
+
if (!byRoot.has(root)) byRoot.set(root, [])
|
|
647
|
+
byRoot.get(root).push(record)
|
|
648
|
+
}
|
|
649
|
+
return Array.from(byRoot.values())
|
|
650
|
+
.filter((group) => group.length >= 2)
|
|
651
|
+
.sort((a, b) => b.length - a.length)
|
|
652
|
+
.slice(0, limit)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function analyzeMemory(args = {}) {
|
|
656
|
+
const project = args.project || projectName()
|
|
657
|
+
const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
|
|
658
|
+
const records = readJsonl(memoryPath).filter(
|
|
659
|
+
(record) => !project || record.project === project || !record.project,
|
|
660
|
+
)
|
|
661
|
+
const archived = records.filter((record) => record.archived === true).length
|
|
662
|
+
const activeRecords = records.filter((record) => record.archived !== true)
|
|
663
|
+
const total = activeRecords.length
|
|
664
|
+
const layerCounts = { project: 0, global: 0 }
|
|
665
|
+
for (const record of activeRecords) {
|
|
666
|
+
if (record.scope === 'global' || !record.project) layerCounts.global += 1
|
|
667
|
+
else layerCounts.project += 1
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
const byKind = {}
|
|
671
|
+
const lowQuality = []
|
|
672
|
+
for (const record of activeRecords) {
|
|
673
|
+
const kind = String(record.kind || '').trim() || 'unknown'
|
|
674
|
+
byKind[kind] = (byKind[kind] || 0) + 1
|
|
675
|
+
const content = String(record.content || '').trim()
|
|
676
|
+
const issues = []
|
|
677
|
+
if (!content) issues.push('missing content')
|
|
678
|
+
else if (content.length < MEMORY_SHORT_CONTENT_MIN) issues.push('content too short')
|
|
679
|
+
if (!record.kind || !String(record.kind).trim()) issues.push('missing kind')
|
|
680
|
+
if (issues.length > 0) {
|
|
681
|
+
lowQuality.push({
|
|
682
|
+
id: record.id,
|
|
683
|
+
timestamp: record.timestamp,
|
|
684
|
+
kind: record.kind || null,
|
|
685
|
+
content: content.slice(0, 200) || null,
|
|
686
|
+
issues,
|
|
687
|
+
})
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let duplicateGroups = []
|
|
692
|
+
let duplicatesSkipped = false
|
|
693
|
+
if (activeRecords.length > MAX_PAIRWISE_MEMORY) {
|
|
694
|
+
duplicatesSkipped = true
|
|
695
|
+
} else {
|
|
696
|
+
duplicateGroups = findDuplicateGroups(activeRecords, limit).map((group) => ({
|
|
697
|
+
size: group.length,
|
|
698
|
+
records: group.map((record) => ({
|
|
699
|
+
id: record.id,
|
|
700
|
+
timestamp: record.timestamp,
|
|
701
|
+
kind: record.kind || null,
|
|
702
|
+
content: String(record.content || '').slice(0, 200),
|
|
703
|
+
})),
|
|
704
|
+
}))
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const issues = {
|
|
708
|
+
lowQuality: lowQuality.length,
|
|
709
|
+
duplicateGroups: duplicateGroups.length,
|
|
710
|
+
duplicatesSkipped,
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const suggestions = []
|
|
714
|
+
if (duplicateGroups.length > 0) {
|
|
715
|
+
suggestions.push(
|
|
716
|
+
`Found ${duplicateGroups.length} duplicate group(s); consider archiving or merging duplicates to keep recall precise.`,
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
if (lowQuality.length > 0) {
|
|
720
|
+
suggestions.push(
|
|
721
|
+
`${lowQuality.length} low-quality record(s) (missing kind/content or too short); consider fixing or removing them.`,
|
|
722
|
+
)
|
|
723
|
+
}
|
|
724
|
+
if (total === 0) {
|
|
725
|
+
suggestions.push('No memory records for this project yet.')
|
|
726
|
+
} else if (duplicateGroups.length === 0 && lowQuality.length === 0) {
|
|
727
|
+
suggestions.push('Memory looks healthy; no dedup or quality fixes needed.')
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
project: project || null,
|
|
732
|
+
total,
|
|
733
|
+
archived,
|
|
734
|
+
layers: layerCounts,
|
|
735
|
+
byKind,
|
|
736
|
+
issues,
|
|
737
|
+
duplicates: duplicateGroups,
|
|
738
|
+
lowQuality: lowQuality.slice(0, limit),
|
|
739
|
+
suggestions,
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function migrateMemory(args = {}) {
|
|
744
|
+
const idsInput = Array.isArray(args.ids) ? args.ids : []
|
|
745
|
+
const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
|
|
746
|
+
if (ids.length === 0) throw new Error('memory.migrate requires ids (array of memory record ids)')
|
|
747
|
+
const targetScope = normalizeScope(args.scope)
|
|
748
|
+
if (targetScope === 'all') throw new Error('memory.migrate scope must be project or global')
|
|
749
|
+
const project = args.project || projectName()
|
|
750
|
+
const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
|
|
751
|
+
|
|
752
|
+
const records = readJsonl(memoryPath)
|
|
753
|
+
const idSet = new Set(ids)
|
|
754
|
+
const updated = []
|
|
755
|
+
const unchanged = []
|
|
756
|
+
const notFound = [...ids]
|
|
757
|
+
for (const record of records) {
|
|
758
|
+
if (!idSet.has(record.id)) continue
|
|
759
|
+
const fromScope = record.scope || 'project'
|
|
760
|
+
const fromProject = record.project || null
|
|
761
|
+
if (fromScope === targetScope) {
|
|
762
|
+
unchanged.push(record.id)
|
|
763
|
+
} else {
|
|
764
|
+
record.scope = targetScope
|
|
765
|
+
record.project = targetScope === 'global' ? null : project
|
|
766
|
+
record.migratedAt = new Date().toISOString()
|
|
767
|
+
record.migratedFrom = fromScope === 'global' ? 'global' : fromProject || 'project'
|
|
768
|
+
if (reason) record.migrateReason = reason
|
|
769
|
+
updated.push(record.id)
|
|
770
|
+
}
|
|
771
|
+
const index = notFound.indexOf(record.id)
|
|
772
|
+
if (index >= 0) notFound.splice(index, 1)
|
|
773
|
+
}
|
|
774
|
+
writeJsonl(memoryPath, records)
|
|
775
|
+
|
|
776
|
+
return {
|
|
777
|
+
project: project || null,
|
|
778
|
+
scope: targetScope,
|
|
779
|
+
updated,
|
|
780
|
+
updatedCount: updated.length,
|
|
781
|
+
unchanged,
|
|
782
|
+
notFound,
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function archiveMemory(args = {}) {
|
|
786
|
+
const idsInput = Array.isArray(args.ids) ? args.ids : []
|
|
787
|
+
const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
|
|
788
|
+
if (ids.length === 0) throw new Error('memory.archive requires ids (array of memory record ids)')
|
|
789
|
+
const project = args.project || projectName()
|
|
790
|
+
const restore = args.restore === true || args.restore === 'true'
|
|
791
|
+
const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
|
|
792
|
+
|
|
793
|
+
const records = readJsonl(memoryPath)
|
|
794
|
+
const idSet = new Set(ids)
|
|
795
|
+
const archived = []
|
|
796
|
+
const notFound = [...ids]
|
|
797
|
+
for (const record of records) {
|
|
798
|
+
if (!idSet.has(record.id)) continue
|
|
799
|
+
if (project && record.project && record.project !== project) continue
|
|
800
|
+
if (restore) {
|
|
801
|
+
if (record.archived === true) {
|
|
802
|
+
delete record.archived
|
|
803
|
+
delete record.archivedAt
|
|
804
|
+
delete record.archiveReason
|
|
805
|
+
archived.push(record.id)
|
|
806
|
+
}
|
|
807
|
+
} else if (record.archived !== true) {
|
|
808
|
+
record.archived = true
|
|
809
|
+
record.archivedAt = new Date().toISOString()
|
|
810
|
+
if (reason) record.archiveReason = reason
|
|
811
|
+
archived.push(record.id)
|
|
812
|
+
}
|
|
813
|
+
const index = notFound.indexOf(record.id)
|
|
814
|
+
if (index >= 0) notFound.splice(index, 1)
|
|
815
|
+
}
|
|
816
|
+
writeJsonl(memoryPath, records)
|
|
817
|
+
|
|
818
|
+
return {
|
|
819
|
+
project: project || null,
|
|
820
|
+
restored: restore,
|
|
821
|
+
archived,
|
|
822
|
+
archivedCount: archived.length,
|
|
823
|
+
notFound,
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const FAILURE_OUTCOMES = new Set(['failure', 'error', 'aborted', 'retry'])
|
|
828
|
+
const MEMORY_DUPLICATE_SIMILARITY = 0.75
|
|
829
|
+
const MEMORY_SHORT_CONTENT_MIN = 20
|
|
830
|
+
const MAX_PAIRWISE_MEMORY = 1500
|
|
831
|
+
|
|
832
|
+
function isFailureOutcome(value) {
|
|
833
|
+
return FAILURE_OUTCOMES.has(String(value || '').toLowerCase())
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function summarizeTrace(event) {
|
|
837
|
+
const payload = event.payload
|
|
838
|
+
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
839
|
+
if (typeof payload.summary === 'string' && payload.summary.trim()) {
|
|
840
|
+
return payload.summary.trim().slice(0, 160)
|
|
841
|
+
}
|
|
842
|
+
if (typeof payload.tool === 'string' && payload.tool.trim()) {
|
|
843
|
+
return `tool: ${payload.tool.trim().slice(0, 120)}`
|
|
844
|
+
}
|
|
845
|
+
if (typeof payload.error === 'string' && payload.error.trim()) {
|
|
846
|
+
return payload.error.trim().slice(0, 160)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return String(event.event_type || 'trace').slice(0, 120)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function buildPolicyGuidance({ total, failures, risk, failureExamples, relatedMemories }) {
|
|
853
|
+
const guidance = {
|
|
854
|
+
level: 'none',
|
|
855
|
+
rationale: null,
|
|
856
|
+
saferAlternatives: [],
|
|
857
|
+
verificationSteps: [],
|
|
858
|
+
avoid: [],
|
|
859
|
+
hardGate: false,
|
|
860
|
+
}
|
|
861
|
+
if (total === 0) {
|
|
862
|
+
guidance.rationale =
|
|
863
|
+
'No historical evidence for this action yet. Treat as normal risk and record the outcome.'
|
|
864
|
+
return guidance
|
|
865
|
+
}
|
|
866
|
+
const verifiedMemories = relatedMemories.filter((record) => record.evidence)
|
|
867
|
+
const hasVerified = verifiedMemories.length > 0
|
|
868
|
+
if (risk >= 0.4 && failures >= 2) {
|
|
869
|
+
guidance.level = hasVerified ? 'actionable' : 'advisory'
|
|
870
|
+
guidance.rationale =
|
|
871
|
+
'Repeated historical failures. Prefer an alternative approach and add rollback/verification.'
|
|
872
|
+
guidance.avoid = failureExamples
|
|
873
|
+
.map((example) => example.summary)
|
|
874
|
+
.filter(Boolean)
|
|
875
|
+
.slice(0, 3)
|
|
876
|
+
} else if (risk >= 0.2 || hasVerified) {
|
|
877
|
+
guidance.level = hasVerified ? 'actionable' : 'advisory'
|
|
878
|
+
guidance.rationale = hasVerified
|
|
879
|
+
? 'Verified experience exists for this action; apply it before proceeding.'
|
|
880
|
+
: 'Moderate historical risk. Add verification before proceeding.'
|
|
881
|
+
} else {
|
|
882
|
+
guidance.level = 'advisory'
|
|
883
|
+
guidance.rationale = 'Low historical risk. Proceed with normal checks and record the outcome.'
|
|
884
|
+
}
|
|
885
|
+
if (hasVerified) {
|
|
886
|
+
guidance.saferAlternatives = verifiedMemories.slice(0, 3).map((record) => ({
|
|
887
|
+
memoryId: record.id,
|
|
888
|
+
reuseCount: record.evidence.reuseCount,
|
|
889
|
+
content: String(record.content || '').slice(0, 300),
|
|
890
|
+
}))
|
|
891
|
+
}
|
|
892
|
+
guidance.verificationSteps = [
|
|
893
|
+
'Add an explicit verification step after execution (dry-run, rollback plan, or assertion).',
|
|
894
|
+
'Record the outcome with mio.observer.ingest so the evidence base improves.',
|
|
895
|
+
]
|
|
896
|
+
return guidance
|
|
897
|
+
}
|
|
898
|
+
|
|
189
899
|
function policyCheck(args = {}) {
|
|
190
900
|
const action = String(args.action || '').trim()
|
|
191
901
|
if (!action) throw new Error('policy.check requires action')
|
|
@@ -199,10 +909,26 @@ function policyCheck(args = {}) {
|
|
|
199
909
|
return actionTokens.some((token) => haystack.includes(token))
|
|
200
910
|
})
|
|
201
911
|
const total = traces.length
|
|
202
|
-
const failures = traces.filter((event) =>
|
|
203
|
-
['failure', 'error', 'aborted', 'retry'].includes(String(event.outcome || '').toLowerCase())
|
|
204
|
-
).length
|
|
912
|
+
const failures = traces.filter((event) => isFailureOutcome(event.outcome)).length
|
|
205
913
|
const risk = total > 0 ? failures / total : null
|
|
914
|
+
const riskLevel = total === 0 ? 'unknown' : risk >= 0.4 ? 'high' : risk >= 0.2 ? 'moderate' : 'low'
|
|
915
|
+
const outcomeCounts = traces.reduce((counts, event) => {
|
|
916
|
+
const outcome = String(event.outcome || 'unknown').toLowerCase() || 'unknown'
|
|
917
|
+
counts[outcome] = (counts[outcome] || 0) + 1
|
|
918
|
+
return counts
|
|
919
|
+
}, {})
|
|
920
|
+
const failureExamples = traces
|
|
921
|
+
.filter((event) => isFailureOutcome(event.outcome))
|
|
922
|
+
.sort((a, b) => new Date(b.timestamp || 0).getTime() - new Date(a.timestamp || 0).getTime())
|
|
923
|
+
.slice(0, 3)
|
|
924
|
+
.map((event) => ({
|
|
925
|
+
trace_id: event.trace_id || event.id || null,
|
|
926
|
+
event_type: event.event_type || null,
|
|
927
|
+
outcome: event.outcome || null,
|
|
928
|
+
timestamp: event.timestamp || null,
|
|
929
|
+
agent: event.agent || null,
|
|
930
|
+
summary: summarizeTrace(event),
|
|
931
|
+
}))
|
|
206
932
|
let suggestion
|
|
207
933
|
if (total === 0) {
|
|
208
934
|
suggestion = 'No history for this action. Treat as normal risk and record the outcome.'
|
|
@@ -213,24 +939,41 @@ function policyCheck(args = {}) {
|
|
|
213
939
|
} else {
|
|
214
940
|
suggestion = 'Low historical risk. Proceed with normal checks.'
|
|
215
941
|
}
|
|
942
|
+
const evidence = loadEvidenceWeights()
|
|
216
943
|
const relatedMemories = readJsonl(memoryPath)
|
|
217
|
-
.filter((record) => scoreRecord(record,
|
|
218
|
-
.sort((a, b) => scoreRecord(b,
|
|
944
|
+
.filter((record) => scoreRecord(record, action, project, evidence) > 0)
|
|
945
|
+
.sort((a, b) => scoreRecord(b, action, project, evidence) - scoreRecord(a, action, project, evidence))
|
|
219
946
|
.slice(0, 3)
|
|
220
|
-
.map((record) =>
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
947
|
+
.map((record) => {
|
|
948
|
+
const ev = evidence.get(record.id)
|
|
949
|
+
return {
|
|
950
|
+
id: record.id,
|
|
951
|
+
timestamp: record.timestamp,
|
|
952
|
+
kind: record.kind,
|
|
953
|
+
content: record.content,
|
|
954
|
+
tags: record.tags,
|
|
955
|
+
evidence: ev
|
|
956
|
+
? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
|
|
957
|
+
: null,
|
|
958
|
+
}
|
|
959
|
+
})
|
|
227
960
|
return {
|
|
228
961
|
action,
|
|
229
962
|
project,
|
|
230
963
|
total,
|
|
231
964
|
failures,
|
|
232
965
|
risk: risk === null ? null : Number(risk.toFixed(3)),
|
|
966
|
+
riskLevel,
|
|
967
|
+
outcomeCounts,
|
|
968
|
+
failureExamples,
|
|
233
969
|
suggestion,
|
|
970
|
+
guidance: buildPolicyGuidance({
|
|
971
|
+
total,
|
|
972
|
+
failures,
|
|
973
|
+
risk,
|
|
974
|
+
failureExamples,
|
|
975
|
+
relatedMemories,
|
|
976
|
+
}),
|
|
234
977
|
related_memories: relatedMemories,
|
|
235
978
|
}
|
|
236
979
|
}
|
|
@@ -243,6 +986,7 @@ function recordExperienceReuse(args = {}) {
|
|
|
243
986
|
if (!targetAgent) throw new Error('experience.reuse requires targetAgent')
|
|
244
987
|
if (!experienceId) throw new Error('experience.reuse requires experienceId')
|
|
245
988
|
|
|
989
|
+
const project = args.project || projectName()
|
|
246
990
|
const evidence = {
|
|
247
991
|
id: createId('xfer'),
|
|
248
992
|
timestamp: new Date().toISOString(),
|
|
@@ -252,11 +996,279 @@ function recordExperienceReuse(args = {}) {
|
|
|
252
996
|
reuse: args.reuse === true || args.reuse === 'true',
|
|
253
997
|
behaviorChanged: args.behaviorChanged === true || args.behaviorChanged === 'true',
|
|
254
998
|
outcomeImproved: args.outcomeImproved === true || args.outcomeImproved === 'true',
|
|
255
|
-
project
|
|
999
|
+
project,
|
|
1000
|
+
source: args.source || 'agent_report',
|
|
256
1001
|
notes: args.notes || null,
|
|
257
1002
|
}
|
|
258
1003
|
appendJsonl(experienceReusePath, evidence)
|
|
259
|
-
|
|
1004
|
+
|
|
1005
|
+
const report = loadPhase0(dataDir, project)
|
|
1006
|
+
return {
|
|
1007
|
+
recorded: true,
|
|
1008
|
+
evidence,
|
|
1009
|
+
phase0: summarizePhase0(report),
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function confirmExperienceReuse(args = {}) {
|
|
1014
|
+
const id = String(args.id || '').trim()
|
|
1015
|
+
if (!id) throw new Error('experience.confirm requires id of an auto-claimed reuse record')
|
|
1016
|
+
const records = readJsonl(experienceReusePath)
|
|
1017
|
+
const index = records.findIndex((record) => record && record.id === id)
|
|
1018
|
+
if (index < 0) {
|
|
1019
|
+
throw new Error(`No experience reuse record found with id ${id}`)
|
|
1020
|
+
}
|
|
1021
|
+
const record = records[index]
|
|
1022
|
+
if (record.source !== 'auto_claim') {
|
|
1023
|
+
throw new Error('experience.confirm only applies to source=auto_claim reuse records')
|
|
1024
|
+
}
|
|
1025
|
+
const outcomeImproved =
|
|
1026
|
+
args.outcomeImproved === undefined || args.outcomeImproved === null
|
|
1027
|
+
? record.outcomeImproved === true
|
|
1028
|
+
: args.outcomeImproved === true || args.outcomeImproved === 'true'
|
|
1029
|
+
const updated = {
|
|
1030
|
+
...record,
|
|
1031
|
+
behaviorChanged: true,
|
|
1032
|
+
outcomeImproved,
|
|
1033
|
+
confirmed: true,
|
|
1034
|
+
confirmedAt: new Date().toISOString(),
|
|
1035
|
+
confirmedBy: String(args.confirmedBy || 'agent').trim() || 'agent',
|
|
1036
|
+
notes: args.notes ? String(args.notes).trim() : record.notes || null,
|
|
1037
|
+
}
|
|
1038
|
+
records[index] = updated
|
|
1039
|
+
writeJsonl(experienceReusePath, records)
|
|
1040
|
+
|
|
1041
|
+
const report = loadPhase0(dataDir, updated.project)
|
|
1042
|
+
return {
|
|
1043
|
+
confirmed: true,
|
|
1044
|
+
evidence: updated,
|
|
1045
|
+
phase0: summarizePhase0(report),
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
const REUSE_STATUS_FILTERS = Object.freeze({
|
|
1050
|
+
pending: (record) => record.source === 'auto_claim' && record.confirmed !== true,
|
|
1051
|
+
confirmed: (record) => record.confirmed === true,
|
|
1052
|
+
verified: (record) =>
|
|
1053
|
+
(record.reuse === true || record.reuse === 'true') &&
|
|
1054
|
+
(record.behaviorChanged === true || record.behaviorChanged === 'true') &&
|
|
1055
|
+
(record.outcomeImproved === true || record.outcomeImproved === 'true'),
|
|
1056
|
+
auto_claim: (record) => record.source === 'auto_claim',
|
|
1057
|
+
agent_report: (record) => record.source !== 'auto_claim',
|
|
1058
|
+
})
|
|
1059
|
+
|
|
1060
|
+
function loadEvidenceWeights() {
|
|
1061
|
+
const weights = new Map()
|
|
1062
|
+
for (const record of readJsonl(experienceReusePath)) {
|
|
1063
|
+
if (!REUSE_STATUS_FILTERS.verified(record)) continue
|
|
1064
|
+
const id = record.experienceId
|
|
1065
|
+
if (!id) continue
|
|
1066
|
+
let entry = weights.get(id)
|
|
1067
|
+
if (!entry) {
|
|
1068
|
+
entry = { reuseCount: 0, confirmedCount: 0, lastReusedAt: null }
|
|
1069
|
+
weights.set(id, entry)
|
|
1070
|
+
}
|
|
1071
|
+
entry.reuseCount += 1
|
|
1072
|
+
if (record.confirmed === true) entry.confirmedCount += 1
|
|
1073
|
+
if (!entry.lastReusedAt || new Date(record.timestamp || 0) > new Date(entry.lastReusedAt)) {
|
|
1074
|
+
entry.lastReusedAt = record.timestamp || null
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return weights
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function listExperienceReuse(args = {}) {
|
|
1081
|
+
const project = args.project || projectName()
|
|
1082
|
+
const status = String(args.status || 'all').trim().toLowerCase()
|
|
1083
|
+
const targetAgent = String(args.targetAgent || '').trim().toLowerCase()
|
|
1084
|
+
const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100)
|
|
1085
|
+
|
|
1086
|
+
let records = readJsonl(experienceReusePath)
|
|
1087
|
+
if (project) records = records.filter((record) => record.project === project)
|
|
1088
|
+
if (targetAgent) {
|
|
1089
|
+
records = records.filter(
|
|
1090
|
+
(record) => String(record.targetAgent || '').toLowerCase() === targetAgent,
|
|
1091
|
+
)
|
|
1092
|
+
}
|
|
1093
|
+
const matcher = REUSE_STATUS_FILTERS[status]
|
|
1094
|
+
if (matcher) records = records.filter(matcher)
|
|
1095
|
+
records.sort((a, b) => {
|
|
1096
|
+
const at = new Date(a.timestamp || 0).getTime()
|
|
1097
|
+
const bt = new Date(b.timestamp || 0).getTime()
|
|
1098
|
+
return bt - at
|
|
1099
|
+
})
|
|
1100
|
+
|
|
1101
|
+
const visible = records.slice(0, limit).map((record) => ({
|
|
1102
|
+
id: record.id,
|
|
1103
|
+
source: record.source,
|
|
1104
|
+
sourceAgent: record.sourceAgent,
|
|
1105
|
+
targetAgent: record.targetAgent,
|
|
1106
|
+
experienceId: record.experienceId,
|
|
1107
|
+
reuse: record.reuse === true || record.reuse === 'true',
|
|
1108
|
+
behaviorChanged: record.behaviorChanged === true || record.behaviorChanged === 'true',
|
|
1109
|
+
outcomeImproved: record.outcomeImproved === true || record.outcomeImproved === 'true',
|
|
1110
|
+
confirmed: record.confirmed === true,
|
|
1111
|
+
confirmedBy: record.confirmedBy || null,
|
|
1112
|
+
traceId: record.traceId || null,
|
|
1113
|
+
timestamp: record.timestamp,
|
|
1114
|
+
project: record.project,
|
|
1115
|
+
notes:
|
|
1116
|
+
typeof record.notes === 'string' && record.notes ? record.notes.slice(0, 300) : null,
|
|
1117
|
+
}))
|
|
1118
|
+
|
|
1119
|
+
return {
|
|
1120
|
+
project: project || null,
|
|
1121
|
+
status,
|
|
1122
|
+
count: visible.length,
|
|
1123
|
+
total: records.length,
|
|
1124
|
+
records: visible,
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function routeTask(args = {}) {
|
|
1129
|
+
const task = String(args.task || '').trim()
|
|
1130
|
+
if (!task) throw new Error('task.route requires a non-empty task')
|
|
1131
|
+
const project = args.project || projectName()
|
|
1132
|
+
const scope = normalizeScope(args.scope)
|
|
1133
|
+
const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 10)
|
|
1134
|
+
|
|
1135
|
+
const memories = readJsonl(memoryPath).filter((record) => record.archived !== true)
|
|
1136
|
+
const memoryById = new Map(memories.map((record) => [record.id, record]))
|
|
1137
|
+
const evidence = loadEvidenceWeights()
|
|
1138
|
+
|
|
1139
|
+
// Match verified reuse experiences whose source memory is relevant to the task.
|
|
1140
|
+
const routeByMemory = new Map()
|
|
1141
|
+
for (const record of readJsonl(experienceReusePath)) {
|
|
1142
|
+
if (!REUSE_STATUS_FILTERS.verified(record)) continue
|
|
1143
|
+
const memory = memoryById.get(record.experienceId)
|
|
1144
|
+
if (!memory || !matchesProjectScope(memory, project, scope)) continue
|
|
1145
|
+
const score = scoreRecord(memory, task, project)
|
|
1146
|
+
if (score <= 0) continue
|
|
1147
|
+
let entry = routeByMemory.get(record.experienceId)
|
|
1148
|
+
if (!entry) {
|
|
1149
|
+
entry = {
|
|
1150
|
+
experienceId: record.experienceId,
|
|
1151
|
+
memory,
|
|
1152
|
+
weight: 0,
|
|
1153
|
+
confirmed: false,
|
|
1154
|
+
sourceAgents: new Set(),
|
|
1155
|
+
targetAgents: new Set(),
|
|
1156
|
+
reuseCount: 0,
|
|
1157
|
+
}
|
|
1158
|
+
routeByMemory.set(record.experienceId, entry)
|
|
1159
|
+
}
|
|
1160
|
+
entry.reuseCount += 1
|
|
1161
|
+
if (record.confirmed === true) entry.confirmed = true
|
|
1162
|
+
if (record.sourceAgent) entry.sourceAgents.add(record.sourceAgent)
|
|
1163
|
+
if (record.targetAgent) entry.targetAgents.add(record.targetAgent)
|
|
1164
|
+
entry.weight = Math.max(entry.weight, score + (entry.confirmed ? 2 : 1.5))
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
const routes = [...routeByMemory.values()]
|
|
1168
|
+
.sort((a, b) => b.weight - a.weight)
|
|
1169
|
+
.slice(0, limit)
|
|
1170
|
+
.map((entry) => ({
|
|
1171
|
+
experienceId: entry.experienceId,
|
|
1172
|
+
reuseCount: entry.reuseCount,
|
|
1173
|
+
confirmed: entry.confirmed,
|
|
1174
|
+
sourceAgents: [...entry.sourceAgents].filter(Boolean),
|
|
1175
|
+
targetAgents: [...entry.targetAgents].filter(Boolean),
|
|
1176
|
+
score: Number(entry.weight.toFixed(2)),
|
|
1177
|
+
memory: {
|
|
1178
|
+
id: entry.memory.id,
|
|
1179
|
+
timestamp: entry.memory.timestamp,
|
|
1180
|
+
kind: entry.memory.kind,
|
|
1181
|
+
content: entry.memory.content,
|
|
1182
|
+
tags: entry.memory.tags || [],
|
|
1183
|
+
project: entry.memory.project || null,
|
|
1184
|
+
scope: entry.memory.scope || 'project',
|
|
1185
|
+
},
|
|
1186
|
+
}))
|
|
1187
|
+
|
|
1188
|
+
// Broader context: top related memories not already routed.
|
|
1189
|
+
const routedIds = new Set(routes.map((route) => route.experienceId))
|
|
1190
|
+
const scoreRelated = (record) =>
|
|
1191
|
+
scoreRecord(record, task, project, evidence) +
|
|
1192
|
+
(scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
|
|
1193
|
+
const relatedMemories = memories
|
|
1194
|
+
.filter((record) => !routedIds.has(record.id))
|
|
1195
|
+
.filter((record) => matchesProjectScope(record, project, scope))
|
|
1196
|
+
.filter((record) => scoreRelated(record) > 0)
|
|
1197
|
+
.sort((a, b) => scoreRelated(b) - scoreRelated(a))
|
|
1198
|
+
.slice(0, Math.max(limit, 5))
|
|
1199
|
+
.map((record) => {
|
|
1200
|
+
const ev = evidence.get(record.id)
|
|
1201
|
+
return {
|
|
1202
|
+
id: record.id,
|
|
1203
|
+
timestamp: record.timestamp,
|
|
1204
|
+
kind: record.kind,
|
|
1205
|
+
content: record.content,
|
|
1206
|
+
tags: record.tags || [],
|
|
1207
|
+
project: record.project || null,
|
|
1208
|
+
scope: record.scope || 'project',
|
|
1209
|
+
evidence: ev
|
|
1210
|
+
? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
|
|
1211
|
+
: null,
|
|
1212
|
+
}
|
|
1213
|
+
})
|
|
1214
|
+
|
|
1215
|
+
// Feed the query log so a later task_outcome can auto-claim route-driven reuse.
|
|
1216
|
+
if (routes.length > 0 || relatedMemories.length > 0) {
|
|
1217
|
+
const now = Date.now()
|
|
1218
|
+
const entries = loadRecentQueries()
|
|
1219
|
+
entries.push({
|
|
1220
|
+
agent: runtimeAgentId() || 'mcp',
|
|
1221
|
+
project,
|
|
1222
|
+
query: task,
|
|
1223
|
+
resultIds: [...routedIds, ...relatedMemories.map((record) => record.id)].filter(Boolean),
|
|
1224
|
+
resultSources: [...routes.map(() => 'experience'), ...relatedMemories.map((record) => record.source || null)],
|
|
1225
|
+
timestamp: now,
|
|
1226
|
+
expiresAt: now + REUSE_MATCH_WINDOW_MS,
|
|
1227
|
+
})
|
|
1228
|
+
persistRecentQueries(entries)
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
let suggestion
|
|
1232
|
+
if (routes.length > 0) {
|
|
1233
|
+
suggestion =
|
|
1234
|
+
`Route ${routes.length} verified experience(s) to this task; apply the top match first and record the outcome with mio.observer.ingest / mio.experience.reuse.`
|
|
1235
|
+
} else if (relatedMemories.length > 0) {
|
|
1236
|
+
suggestion =
|
|
1237
|
+
'No verified experience matches yet. Consult the related memories, then record the outcome so this task can seed future routes.'
|
|
1238
|
+
} else {
|
|
1239
|
+
suggestion =
|
|
1240
|
+
'No matching experience or memory found. Proceed with care and record the outcome to grow the experience base.'
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
return {
|
|
1244
|
+
task,
|
|
1245
|
+
project,
|
|
1246
|
+
scope,
|
|
1247
|
+
count: routes.length,
|
|
1248
|
+
routes,
|
|
1249
|
+
relatedMemories,
|
|
1250
|
+
summary: {
|
|
1251
|
+
verifiedRoutes: routes.length,
|
|
1252
|
+
relatedMemories: relatedMemories.length,
|
|
1253
|
+
suggestion,
|
|
1254
|
+
},
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function phase0Report(args = {}) {
|
|
1258
|
+
const project = args.project || projectName()
|
|
1259
|
+
const report = loadPhase0(dataDir, project)
|
|
1260
|
+
const format = String(args.format || 'json').trim().toLowerCase()
|
|
1261
|
+
if (format === 'markdown') {
|
|
1262
|
+
return {
|
|
1263
|
+
project,
|
|
1264
|
+
format: 'markdown',
|
|
1265
|
+
markdown: renderPhase0Markdown(report),
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return {
|
|
1269
|
+
project,
|
|
1270
|
+
...report,
|
|
1271
|
+
}
|
|
260
1272
|
}
|
|
261
1273
|
|
|
262
1274
|
const TOOLS = [
|
|
@@ -269,10 +1281,41 @@ const TOOLS = [
|
|
|
269
1281
|
query: { type: 'string', description: 'Question or topic to recall.' },
|
|
270
1282
|
project: { type: 'string', description: 'Project name filter. Defaults to current directory name.' },
|
|
271
1283
|
limit: { type: 'number', description: 'Maximum results, between 1 and 20. Defaults to 5.' },
|
|
1284
|
+
kind: { type: 'string', description: 'Only return records with this kind (decision, context, problem, note).' },
|
|
1285
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Only return records containing all of these tags (AND).' },
|
|
1286
|
+
scope: { type: 'string', description: 'Search scope: project (default), global, or all.' },
|
|
272
1287
|
},
|
|
273
1288
|
required: ['query'],
|
|
274
1289
|
},
|
|
275
1290
|
},
|
|
1291
|
+
{
|
|
1292
|
+
name: 'mio.memory.archive',
|
|
1293
|
+
description: 'Archive (or restore) memory records by id. Archived records are excluded from memory.query and memory.analyze.',
|
|
1294
|
+
inputSchema: {
|
|
1295
|
+
type: 'object',
|
|
1296
|
+
properties: {
|
|
1297
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to archive or restore.' },
|
|
1298
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
1299
|
+
restore: { type: 'boolean', description: 'When true, un-archive the given ids instead.' },
|
|
1300
|
+
reason: { type: 'string', description: 'Optional archive reason.' },
|
|
1301
|
+
},
|
|
1302
|
+
required: ['ids'],
|
|
1303
|
+
},
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
name: 'mio.memory.migrate',
|
|
1307
|
+
description: 'Migrate memory records between project and global layers (P3). Moving to global makes the record reusable across projects (project set to null); moving to project restores project scoping.',
|
|
1308
|
+
inputSchema: {
|
|
1309
|
+
type: 'object',
|
|
1310
|
+
properties: {
|
|
1311
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to migrate.' },
|
|
1312
|
+
scope: { type: 'string', description: 'Target layer: global or project.' },
|
|
1313
|
+
project: { type: 'string', description: 'Target project for project scope. Defaults to current repository.' },
|
|
1314
|
+
reason: { type: 'string', description: 'Optional migration reason.' },
|
|
1315
|
+
},
|
|
1316
|
+
required: ['ids', 'scope'],
|
|
1317
|
+
},
|
|
1318
|
+
},
|
|
276
1319
|
{
|
|
277
1320
|
name: 'mio.memory.record',
|
|
278
1321
|
description: 'Record a decision, project context, or historical problem into local Mio memory.',
|
|
@@ -283,6 +1326,7 @@ const TOOLS = [
|
|
|
283
1326
|
kind: { type: 'string', description: 'Memory kind, such as decision, context, or problem.' },
|
|
284
1327
|
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
|
|
285
1328
|
project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
|
|
1329
|
+
scope: { type: 'string', description: 'Record scope: project (default) or global.' },
|
|
286
1330
|
source: { type: 'string', description: 'Recording source. Defaults to mcp.' },
|
|
287
1331
|
},
|
|
288
1332
|
required: ['content'],
|
|
@@ -305,9 +1349,34 @@ const TOOLS = [
|
|
|
305
1349
|
required: ['trace_id', 'event_type'],
|
|
306
1350
|
},
|
|
307
1351
|
},
|
|
1352
|
+
{
|
|
1353
|
+
name: 'mio.observer.subscribe',
|
|
1354
|
+
description: 'Subscribe to observer events (project, event types, optional topic). New matching events are delivered via mio.observer.digest.',
|
|
1355
|
+
inputSchema: {
|
|
1356
|
+
type: 'object',
|
|
1357
|
+
properties: {
|
|
1358
|
+
eventTypes: { type: 'array', items: { type: 'string' }, description: 'Event types to watch, e.g. error, retry, task_outcome. Empty means all.' },
|
|
1359
|
+
project: { type: 'string', description: 'Project name. Defaults to current repository.' },
|
|
1360
|
+
topic: { type: 'string', description: 'Optional keyword; only events whose payload mentions it match.' },
|
|
1361
|
+
ttlDays: { type: 'number', description: 'Subscription lifetime in days. Defaults to 30.' },
|
|
1362
|
+
},
|
|
1363
|
+
},
|
|
1364
|
+
},
|
|
1365
|
+
{
|
|
1366
|
+
name: 'mio.observer.digest',
|
|
1367
|
+
description: 'Return new observer events matching active subscriptions since the last digest, and advance the cursor.',
|
|
1368
|
+
inputSchema: {
|
|
1369
|
+
type: 'object',
|
|
1370
|
+
properties: {
|
|
1371
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
1372
|
+
eventTypes: { type: 'array', items: { type: 'string' }, description: 'Optional extra event type filter.' },
|
|
1373
|
+
limit: { type: 'number', description: 'Maximum events to return, 1-100. Defaults to 20.' },
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
},
|
|
308
1377
|
{
|
|
309
1378
|
name: 'mio.policy.check',
|
|
310
|
-
description: 'Check historical risk for an action before executing it.',
|
|
1379
|
+
description: 'Check historical risk for an action before executing it, returning risk level, outcome counts, and recent failure examples.',
|
|
311
1380
|
inputSchema: {
|
|
312
1381
|
type: 'object',
|
|
313
1382
|
properties: {
|
|
@@ -318,6 +1387,17 @@ const TOOLS = [
|
|
|
318
1387
|
required: ['action'],
|
|
319
1388
|
},
|
|
320
1389
|
},
|
|
1390
|
+
{
|
|
1391
|
+
name: 'mio.memory.analyze',
|
|
1392
|
+
description: 'Analyze local Mio memory quality: kind distribution, duplicate groups, and low-quality records.',
|
|
1393
|
+
inputSchema: {
|
|
1394
|
+
type: 'object',
|
|
1395
|
+
properties: {
|
|
1396
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
1397
|
+
limit: { type: 'number', description: 'Maximum duplicates/low-quality examples to return, 1-20. Defaults to 5.' },
|
|
1398
|
+
},
|
|
1399
|
+
},
|
|
1400
|
+
},
|
|
321
1401
|
{
|
|
322
1402
|
name: 'mio.experience.reuse',
|
|
323
1403
|
description: 'Record Phase 0 evidence of cross-agent experience reuse.',
|
|
@@ -336,6 +1416,58 @@ const TOOLS = [
|
|
|
336
1416
|
required: ['sourceAgent', 'targetAgent', 'experienceId', 'reuse', 'behaviorChanged', 'outcomeImproved'],
|
|
337
1417
|
},
|
|
338
1418
|
},
|
|
1419
|
+
{
|
|
1420
|
+
name: 'mio.experience.confirm',
|
|
1421
|
+
description: 'Confirm an auto-claimed reuse record as verified reuse evidence.',
|
|
1422
|
+
inputSchema: {
|
|
1423
|
+
type: 'object',
|
|
1424
|
+
properties: {
|
|
1425
|
+
id: { type: 'string', description: 'Id of the auto-claimed reuse record returned by mio.observer.ingest.' },
|
|
1426
|
+
confirmedBy: { type: 'string', description: 'Who confirmed the reuse: agent or human. Defaults to agent.' },
|
|
1427
|
+
outcomeImproved: { type: 'boolean', description: 'Override: true when the reused experience improved the task outcome. Defaults to the auto-claim value.' },
|
|
1428
|
+
notes: { type: 'string', description: 'Optional confirmation notes.' },
|
|
1429
|
+
},
|
|
1430
|
+
required: ['id'],
|
|
1431
|
+
},
|
|
1432
|
+
},
|
|
1433
|
+
{
|
|
1434
|
+
name: 'mio.experience.list',
|
|
1435
|
+
description: 'List Phase 0 experience reuse evidence, optionally filtered by status or target agent.',
|
|
1436
|
+
inputSchema: {
|
|
1437
|
+
type: 'object',
|
|
1438
|
+
properties: {
|
|
1439
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
1440
|
+
status: { type: 'string', description: 'Filter: pending (auto-claim awaiting confirmation), confirmed, verified, auto_claim, agent_report, all. Defaults to all.' },
|
|
1441
|
+
targetAgent: { type: 'string', description: 'Filter records by target agent.' },
|
|
1442
|
+
limit: { type: 'number', description: 'Maximum records to return, 1-100. Defaults to 20.' },
|
|
1443
|
+
},
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
{
|
|
1447
|
+
name: 'mio.phase0.report',
|
|
1448
|
+
description: 'Generate the current Phase 0 cross-agent validation report, as JSON or rendered Markdown.',
|
|
1449
|
+
inputSchema: {
|
|
1450
|
+
type: 'object',
|
|
1451
|
+
properties: {
|
|
1452
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
1453
|
+
format: { type: 'string', description: 'Output format: json or markdown. Defaults to json.' },
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
name: 'mio.task.route',
|
|
1459
|
+
description: 'Route an incoming task to the most relevant verified experiences and related memories, turning recall into decision influence (P1).',
|
|
1460
|
+
inputSchema: {
|
|
1461
|
+
type: 'object',
|
|
1462
|
+
properties: {
|
|
1463
|
+
task: { type: 'string', description: 'Task description to route, e.g. \'fix IPC communication bug\'.' },
|
|
1464
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
1465
|
+
scope: { type: 'string', description: 'Search scope: project (default), global, or all.' },
|
|
1466
|
+
limit: { type: 'number', description: 'Maximum routes, between 1 and 10. Defaults to 5.' },
|
|
1467
|
+
},
|
|
1468
|
+
required: ['task'],
|
|
1469
|
+
},
|
|
1470
|
+
},
|
|
339
1471
|
]
|
|
340
1472
|
|
|
341
1473
|
async function callTool(name, args = {}) {
|
|
@@ -344,12 +1476,30 @@ async function callTool(name, args = {}) {
|
|
|
344
1476
|
return queryMemory(args)
|
|
345
1477
|
case 'mio.memory.record':
|
|
346
1478
|
return recordMemory(args)
|
|
1479
|
+
case 'mio.memory.archive':
|
|
1480
|
+
return archiveMemory(args)
|
|
1481
|
+
case 'mio.memory.migrate':
|
|
1482
|
+
return migrateMemory(args)
|
|
347
1483
|
case 'mio.observer.ingest':
|
|
348
1484
|
return ingestObservation(args)
|
|
1485
|
+
case 'mio.observer.subscribe':
|
|
1486
|
+
return subscribeObserver(args)
|
|
1487
|
+
case 'mio.observer.digest':
|
|
1488
|
+
return observerDigest(args)
|
|
349
1489
|
case 'mio.policy.check':
|
|
350
1490
|
return policyCheck(args)
|
|
1491
|
+
case 'mio.memory.analyze':
|
|
1492
|
+
return analyzeMemory(args)
|
|
351
1493
|
case 'mio.experience.reuse':
|
|
352
1494
|
return recordExperienceReuse(args)
|
|
1495
|
+
case 'mio.experience.confirm':
|
|
1496
|
+
return confirmExperienceReuse(args)
|
|
1497
|
+
case 'mio.experience.list':
|
|
1498
|
+
return listExperienceReuse(args)
|
|
1499
|
+
case 'mio.phase0.report':
|
|
1500
|
+
return phase0Report(args)
|
|
1501
|
+
case 'mio.task.route':
|
|
1502
|
+
return routeTask(args)
|
|
353
1503
|
default:
|
|
354
1504
|
throw new Error(`Unknown tool: ${name}`)
|
|
355
1505
|
}
|
|
@@ -426,7 +1576,9 @@ rl.on('line', (line) => {
|
|
|
426
1576
|
})
|
|
427
1577
|
|
|
428
1578
|
rl.on('close', () => {
|
|
429
|
-
|
|
1579
|
+
if (require.main === module) {
|
|
1580
|
+
process.exit(0)
|
|
1581
|
+
}
|
|
430
1582
|
})
|
|
431
1583
|
|
|
432
|
-
module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl }
|
|
1584
|
+
module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl, rl }
|