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
|
@@ -10,75 +10,32 @@ const { getEvolutionStatus } = require('../runtime-modules.js')
|
|
|
10
10
|
const { listHostCapabilities } = require('../host-capabilities.js')
|
|
11
11
|
const { createEvolutionCutoverTools } = require('../evolution-cutover.js')
|
|
12
12
|
const { createMemoryStore } = require('../memory-store.js')
|
|
13
|
+
const { createExperienceStore, REUSE_STATUS_FILTERS } = require('../experience-store.js')
|
|
14
|
+
const { createPolicyStore } = require('../policy-store.js')
|
|
15
|
+
const { createAgentStore } = require('../agent-store.js')
|
|
16
|
+
const { createTaskStore } = require('../task-store.js')
|
|
17
|
+
const { createQueryLog } = require('../query-log.js')
|
|
18
|
+
const { createSubscriptionStore } = require('../subscription-store.js')
|
|
13
19
|
const { createDigest } = require('../digest.js')
|
|
20
|
+
const { createEvolutionReport } = require('../evolution-report.js')
|
|
14
21
|
|
|
15
22
|
const { CreativityEngine } = require('../creativity-engine.js')
|
|
16
|
-
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ObserverStore = obs.ObserverStore
|
|
23
|
-
ObserverService = obs.ObserverService
|
|
24
|
-
} catch {}
|
|
25
|
-
|
|
26
|
-
// Insight self-observation engine (optional — installed via @akemi-mio/insight)
|
|
27
|
-
let InsightStore = null
|
|
28
|
-
let InsightGenerator = null
|
|
29
|
-
let PresenceService = null
|
|
30
|
-
try {
|
|
31
|
-
const insight = require('@akemi-mio/insight')
|
|
32
|
-
InsightStore = insight.InsightStore
|
|
33
|
-
InsightGenerator = insight.InsightGenerator
|
|
34
|
-
PresenceService = insight.PresenceService
|
|
35
|
-
} catch {}
|
|
23
|
+
// The observer and insight engines are optional dependencies. Their stores own
|
|
24
|
+
// the require and expose isObserverAvailable() / isInsightAvailable() so tool
|
|
25
|
+
// registration can be gated on them, and both the MCP server and the CLI share
|
|
26
|
+
// one implementation (see ../observer-store.js and ../insight-store.js).
|
|
27
|
+
const { createObserverStore, isObserverAvailable } = require('../observer-store.js')
|
|
28
|
+
const { createInsightStore, isInsightAvailable } = require('../insight-store.js')
|
|
36
29
|
|
|
37
30
|
const SERVER_INFO = { name: 'mio-intelligence-mcp', version: '0.1.0' }
|
|
38
31
|
const PROTOCOL_VERSION = '2024-11-05'
|
|
39
32
|
|
|
40
33
|
// ═══════════════════════════════════════════════
|
|
41
|
-
// LLM chatJson —
|
|
34
|
+
// LLM chatJson — shared with the CLI (see ../llm-client.js)
|
|
42
35
|
// ═══════════════════════════════════════════════
|
|
43
36
|
|
|
44
|
-
|
|
45
|
-
const apiUrl = process.env.LLM_API_URL || 'https://opencode.ai/zen/go/v1/chat/completions'
|
|
46
|
-
const apiKey = process.env.LLM_KEY || ''
|
|
47
|
-
const model = process.env.LLM_CHAT_MODEL || process.env.LLM_MODEL || 'deepseek-v4-flash'
|
|
48
|
-
const system = opts.system || 'You are a creative AI assistant. Output JSON.'
|
|
49
|
-
const temperature = opts.temperature ?? 0.3
|
|
37
|
+
const { chatJson } = require('../llm-client.js')
|
|
50
38
|
|
|
51
|
-
try {
|
|
52
|
-
const resp = await fetch(apiUrl, {
|
|
53
|
-
method: 'POST',
|
|
54
|
-
headers: {
|
|
55
|
-
'Content-Type': 'application/json',
|
|
56
|
-
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
|
57
|
-
},
|
|
58
|
-
body: JSON.stringify({
|
|
59
|
-
model,
|
|
60
|
-
temperature,
|
|
61
|
-
messages: [
|
|
62
|
-
{ role: 'system', content: system },
|
|
63
|
-
{ role: 'user', content: userText },
|
|
64
|
-
],
|
|
65
|
-
response_format: { type: 'json_object' },
|
|
66
|
-
}),
|
|
67
|
-
signal: AbortSignal.timeout(opts.timeoutMs || 30000),
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
if (!resp.ok) return { error: `HTTP ${resp.status}` }
|
|
71
|
-
const json = await resp.json()
|
|
72
|
-
const text = json.choices?.[0]?.message?.content || ''
|
|
73
|
-
try {
|
|
74
|
-
return { data: JSON.parse(text) }
|
|
75
|
-
} catch {
|
|
76
|
-
return { data: text }
|
|
77
|
-
}
|
|
78
|
-
} catch (err) {
|
|
79
|
-
return { error: String(err) }
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
39
|
|
|
83
40
|
const dataDir = path.resolve(
|
|
84
41
|
process.env.MIO_DATA_DIR || path.join(process.cwd(), '.mio-intelligence')
|
|
@@ -95,7 +52,10 @@ fs.mkdirSync(dataDir, { recursive: true })
|
|
|
95
52
|
const { loadPhase0, summarizePhase0, renderPhase0Markdown } = require('./phase0')
|
|
96
53
|
const evolutionCutover = createEvolutionCutoverTools({ dataDir, appendJsonl, readJsonl, projectName })
|
|
97
54
|
const creativityEngine = new CreativityEngine(path.join(dataDir, 'creativity'), chatJson)
|
|
98
|
-
const insightStore =
|
|
55
|
+
const insightStore = createInsightStore({ dataDir, chatJson })
|
|
56
|
+
// baseDir defaults to <cwd>/.local/observer, matching the previous inline
|
|
57
|
+
// behaviour; individual calls may still override it via args.baseDir.
|
|
58
|
+
const observerStore = createObserverStore({})
|
|
99
59
|
|
|
100
60
|
let runtimeContext = {}
|
|
101
61
|
try {
|
|
@@ -117,40 +77,6 @@ function runtimeAgentId() {
|
|
|
117
77
|
// Phase 0 auto-claim: match memory.query results with a later task_outcome
|
|
118
78
|
// from the same agent+project, so reuse evidence does not depend on the
|
|
119
79
|
// agent self-reporting mio.experience.reuse.
|
|
120
|
-
const REUSE_MATCH_WINDOW_MS = (() => {
|
|
121
|
-
const minutes = Number(process.env.MIO_REUSE_MATCH_WINDOW_MIN)
|
|
122
|
-
return Number.isFinite(minutes) && minutes > 0 ? minutes * 60 * 1000 : 60 * 60 * 1000
|
|
123
|
-
})()
|
|
124
|
-
const MAX_RECENT_QUERIES = 200
|
|
125
|
-
let recentQueries = []
|
|
126
|
-
|
|
127
|
-
function loadRecentQueries() {
|
|
128
|
-
const now = Date.now()
|
|
129
|
-
const entries = readJsonl(queryPath)
|
|
130
|
-
.filter(
|
|
131
|
-
(entry) =>
|
|
132
|
-
entry &&
|
|
133
|
-
typeof entry.agent === 'string' &&
|
|
134
|
-
entry.agent &&
|
|
135
|
-
typeof entry.expiresAt === 'number' &&
|
|
136
|
-
entry.expiresAt > now
|
|
137
|
-
)
|
|
138
|
-
.slice(-MAX_RECENT_QUERIES)
|
|
139
|
-
recentQueries = entries
|
|
140
|
-
return entries
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function persistRecentQueries(entries) {
|
|
144
|
-
const limited = entries.slice(-MAX_RECENT_QUERIES)
|
|
145
|
-
fs.mkdirSync(dataDir, { recursive: true })
|
|
146
|
-
fs.writeFileSync(
|
|
147
|
-
queryPath,
|
|
148
|
-
limited.length > 0 ? limited.map((entry) => JSON.stringify(entry)).join('\n') + '\n' : '',
|
|
149
|
-
'utf8'
|
|
150
|
-
)
|
|
151
|
-
recentQueries = limited
|
|
152
|
-
return limited
|
|
153
|
-
}
|
|
154
80
|
|
|
155
81
|
function createId(prefix) {
|
|
156
82
|
return `${prefix}_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
|
|
@@ -226,28 +152,29 @@ function projectName() {
|
|
|
226
152
|
// Memory store: shared implementation with the CLI (`mio recall`/`mio remember`).
|
|
227
153
|
// The pure scoring/filter helpers and queryMemory/recordMemory live in
|
|
228
154
|
// server/memory-store.js so both entry points rank and persist identically.
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
(record.behaviorChanged === true || record.behaviorChanged === 'true') &&
|
|
235
|
-
(record.outcomeImproved === true || record.outcomeImproved === 'true'),
|
|
236
|
-
auto_claim: (record) => record.source === 'auto_claim',
|
|
237
|
-
agent_report: (record) => record.source !== 'auto_claim',
|
|
155
|
+
const experienceStore = createExperienceStore({
|
|
156
|
+
dataDir,
|
|
157
|
+
projectName,
|
|
158
|
+
// Preserves the phase0 summary these tools have always returned.
|
|
159
|
+
phase0Summary: (project) => summarizePhase0(loadPhase0(dataDir, project)),
|
|
238
160
|
})
|
|
161
|
+
const {
|
|
162
|
+
listReuse: listExperienceReuse,
|
|
163
|
+
confirmReuse: confirmExperienceReuse,
|
|
164
|
+
recordReuse: recordExperienceReuse,
|
|
165
|
+
} = experienceStore
|
|
166
|
+
|
|
167
|
+
// One query-log instance shared by the memory store (which records queries) and
|
|
168
|
+
// the task store (which consumes them for auto-claim). This is what removes the
|
|
169
|
+
// previous duplicate load/persist implementations of queries.jsonl.
|
|
170
|
+
const queryLog = createQueryLog({ dataDir })
|
|
239
171
|
|
|
240
172
|
const memoryStore = createMemoryStore({
|
|
241
173
|
dataDir,
|
|
242
174
|
projectName,
|
|
243
175
|
agentId: runtimeAgentId,
|
|
244
|
-
|
|
176
|
+
queryLog,
|
|
245
177
|
verifiedFilter: REUSE_STATUS_FILTERS.verified,
|
|
246
|
-
recordQuery(entry) {
|
|
247
|
-
const entries = loadRecentQueries()
|
|
248
|
-
entries.push(entry)
|
|
249
|
-
persistRecentQueries(entries)
|
|
250
|
-
},
|
|
251
178
|
})
|
|
252
179
|
|
|
253
180
|
const digestEngine = createDigest({ home: dataDir })
|
|
@@ -265,978 +192,49 @@ const {
|
|
|
265
192
|
queryMemory,
|
|
266
193
|
recordMemory,
|
|
267
194
|
queryTraces,
|
|
195
|
+
analyzeMemory,
|
|
196
|
+
archiveMemory,
|
|
197
|
+
forgetMemory,
|
|
198
|
+
mergeMemory,
|
|
199
|
+
migrateMemory,
|
|
268
200
|
loadEvidenceWeights,
|
|
269
201
|
} = memoryStore
|
|
270
202
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
outcome: args.outcome || null,
|
|
282
|
-
payload: args.payload || {},
|
|
283
|
-
agent: args.agent || runtimeAgentId(),
|
|
284
|
-
host: args.host || 'mcp',
|
|
285
|
-
project: args.project || projectName(),
|
|
286
|
-
}
|
|
287
|
-
appendJsonl(tracePath, event)
|
|
288
|
-
const autoClaims = autoClaimExperienceReuse(event)
|
|
289
|
-
if (autoClaims.length > 0) {
|
|
290
|
-
return { recorded: true, event, autoClaims }
|
|
291
|
-
}
|
|
292
|
-
return { recorded: true, event }
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function autoClaimExperienceReuse(event) {
|
|
296
|
-
if (String(event.event_type || '').toLowerCase() !== 'task_outcome') return []
|
|
297
|
-
const targetAgent = String(event.agent || '').trim()
|
|
298
|
-
if (!targetAgent) return []
|
|
299
|
-
const now = Date.now()
|
|
300
|
-
const claims = []
|
|
301
|
-
const remaining = []
|
|
302
|
-
const entries = loadRecentQueries()
|
|
303
|
-
for (const entry of entries) {
|
|
304
|
-
const expired = now - entry.timestamp > REUSE_MATCH_WINDOW_MS
|
|
305
|
-
const matches =
|
|
306
|
-
entry.agent === targetAgent && (!event.project || entry.project === event.project)
|
|
307
|
-
if (expired || matches) {
|
|
308
|
-
if (matches && !expired) {
|
|
309
|
-
claims.push(buildAutoClaim(entry, event, targetAgent))
|
|
310
|
-
}
|
|
311
|
-
continue
|
|
312
|
-
}
|
|
313
|
-
remaining.push(entry)
|
|
314
|
-
}
|
|
315
|
-
persistRecentQueries(remaining)
|
|
316
|
-
for (const evidence of claims) {
|
|
317
|
-
appendJsonl(experienceReusePath, evidence)
|
|
318
|
-
}
|
|
319
|
-
return claims
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function buildAutoClaim(entry, event, targetAgent) {
|
|
323
|
-
const crossSourceIndex = entry.resultSources.findIndex(
|
|
324
|
-
(source) => source && String(source).toLowerCase() !== targetAgent.toLowerCase()
|
|
325
|
-
)
|
|
326
|
-
const sourceAgent =
|
|
327
|
-
crossSourceIndex >= 0
|
|
328
|
-
? String(entry.resultSources[crossSourceIndex])
|
|
329
|
-
: targetAgent
|
|
330
|
-
const experienceId =
|
|
331
|
-
entry.resultIds[crossSourceIndex >= 0 ? crossSourceIndex : 0] ||
|
|
332
|
-
entry.resultIds[0] ||
|
|
333
|
-
'unknown'
|
|
334
|
-
const outcomeImproved = String(event.outcome || '').toLowerCase() === 'success'
|
|
335
|
-
return {
|
|
336
|
-
id: createId('xfer'),
|
|
337
|
-
timestamp: new Date().toISOString(),
|
|
338
|
-
sourceAgent,
|
|
339
|
-
targetAgent,
|
|
340
|
-
experienceId,
|
|
341
|
-
reuse: true,
|
|
342
|
-
behaviorChanged: false,
|
|
343
|
-
outcomeImproved,
|
|
344
|
-
project: entry.project,
|
|
345
|
-
source: 'auto_claim',
|
|
346
|
-
traceId: event.trace_id || null,
|
|
347
|
-
notes: `Auto-claimed: memory.query "${entry.query}" matched task_outcome ${event.outcome || 'unknown'} in trace ${event.trace_id || 'unknown'}.`,
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const SUBSCRIPTION_DEFAULT_TTL_DAYS = 30
|
|
352
|
-
const MAX_SUBSCRIPTIONS_PER_AGENT = 20
|
|
353
|
-
const MAX_DIGEST_EVENTS = 100
|
|
354
|
-
|
|
355
|
-
function readDigestState() {
|
|
356
|
-
if (!fs.existsSync(digestStatePath)) return {}
|
|
357
|
-
try {
|
|
358
|
-
const parsed = JSON.parse(fs.readFileSync(digestStatePath, 'utf8'))
|
|
359
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}
|
|
360
|
-
} catch (_) {
|
|
361
|
-
return {}
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function writeDigestState(state) {
|
|
366
|
-
fs.mkdirSync(dataDir, { recursive: true })
|
|
367
|
-
fs.writeFileSync(digestStatePath, JSON.stringify(state), 'utf8')
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function subscribeKey(agent, project, eventTypes, topic) {
|
|
371
|
-
return `${agent}|${project}|${eventTypes.join(',')}|${topic || ''}`
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function loadActiveSubscriptions(agent) {
|
|
375
|
-
const now = Date.now()
|
|
376
|
-
return readJsonl(subscriptionPath)
|
|
377
|
-
.filter((subscription) => subscription && subscription.agent === agent)
|
|
378
|
-
.filter((subscription) => {
|
|
379
|
-
if (subscription.expiresAt) {
|
|
380
|
-
const expires = new Date(subscription.expiresAt).getTime()
|
|
381
|
-
if (Number.isFinite(expires) && expires <= now) return false
|
|
382
|
-
}
|
|
383
|
-
return true
|
|
384
|
-
})
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
function subscribeObserver(args = {}) {
|
|
388
|
-
const agent = runtimeAgentId() || 'mcp'
|
|
389
|
-
const project = args.project || projectName()
|
|
390
|
-
const eventTypes = Array.isArray(args.eventTypes)
|
|
391
|
-
? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
|
|
392
|
-
: []
|
|
393
|
-
const topic = args.topic ? String(args.topic).trim().slice(0, 120) : ''
|
|
394
|
-
const ttlDays = Number(args.ttlDays) || SUBSCRIPTION_DEFAULT_TTL_DAYS
|
|
395
|
-
const ttlMs = Math.max(1, ttlDays) * 24 * 60 * 60 * 1000
|
|
396
|
-
|
|
397
|
-
const subscriptions = readJsonl(subscriptionPath)
|
|
398
|
-
const key = subscribeKey(agent, project, eventTypes, topic)
|
|
399
|
-
let existing = subscriptions.find((subscription) => {
|
|
400
|
-
return (
|
|
401
|
-
subscription &&
|
|
402
|
-
subscribeKey(
|
|
403
|
-
subscription.agent,
|
|
404
|
-
subscription.project,
|
|
405
|
-
subscription.eventTypes || [],
|
|
406
|
-
subscription.topic || '',
|
|
407
|
-
) === key
|
|
408
|
-
)
|
|
409
|
-
})
|
|
410
|
-
const now = new Date()
|
|
411
|
-
const expiresAt = new Date(now.getTime() + ttlMs).toISOString()
|
|
412
|
-
if (existing) {
|
|
413
|
-
existing.updatedAt = now.toISOString()
|
|
414
|
-
existing.expiresAt = expiresAt
|
|
415
|
-
} else {
|
|
416
|
-
existing = {
|
|
417
|
-
id: createId('sub'),
|
|
418
|
-
agent,
|
|
419
|
-
project,
|
|
420
|
-
eventTypes,
|
|
421
|
-
topic: topic || null,
|
|
422
|
-
createdAt: now.toISOString(),
|
|
423
|
-
updatedAt: now.toISOString(),
|
|
424
|
-
expiresAt,
|
|
425
|
-
}
|
|
426
|
-
subscriptions.push(existing)
|
|
427
|
-
}
|
|
428
|
-
const agentSubscriptions = subscriptions.filter((subscription) => subscription.agent === agent)
|
|
429
|
-
if (agentSubscriptions.length > MAX_SUBSCRIPTIONS_PER_AGENT) {
|
|
430
|
-
throw new Error(`Too many subscriptions for agent ${agent} (max ${MAX_SUBSCRIPTIONS_PER_AGENT})`)
|
|
431
|
-
}
|
|
432
|
-
writeJsonl(subscriptionPath, subscriptions)
|
|
433
|
-
return { subscribed: true, subscription: existing, project }
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function observerDigest(args = {}) {
|
|
437
|
-
const agent = runtimeAgentId() || 'mcp'
|
|
438
|
-
const project = args.project || projectName()
|
|
439
|
-
const limit = Math.min(Math.max(Number(args.limit) || 20, 1), MAX_DIGEST_EVENTS)
|
|
440
|
-
const filterEventTypes = Array.isArray(args.eventTypes)
|
|
441
|
-
? args.eventTypes.map((value) => String(value).trim().toLowerCase()).filter(Boolean)
|
|
442
|
-
: null
|
|
443
|
-
|
|
444
|
-
let subscriptions = loadActiveSubscriptions(agent)
|
|
445
|
-
if (args.project) {
|
|
446
|
-
subscriptions = subscriptions.filter((subscription) => subscription.project === project)
|
|
447
|
-
}
|
|
448
|
-
if (filterEventTypes) {
|
|
449
|
-
subscriptions = subscriptions.filter((subscription) => {
|
|
450
|
-
if ((subscription.eventTypes || []).length === 0) return true
|
|
451
|
-
return filterEventTypes.some((eventType) =>
|
|
452
|
-
(subscription.eventTypes || []).includes(eventType),
|
|
453
|
-
)
|
|
454
|
-
})
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
const state = readDigestState()
|
|
458
|
-
const traces = readJsonl(tracePath)
|
|
459
|
-
const digestTime = new Date().toISOString()
|
|
460
|
-
const events = []
|
|
461
|
-
const seenIds = new Set()
|
|
462
|
-
const matchedSubscriptionIds = new Set()
|
|
463
|
-
|
|
464
|
-
for (const subscription of subscriptions) {
|
|
465
|
-
const cursor = state[subscription.id] || null
|
|
466
|
-
const cursorTime = cursor ? Date.parse(cursor) : null
|
|
467
|
-
const subscriptionEventTypes = subscription.eventTypes || []
|
|
468
|
-
let lastEventTime = null
|
|
469
|
-
for (const event of traces) {
|
|
470
|
-
if (subscription.project && event.project && event.project !== subscription.project) {
|
|
471
|
-
continue
|
|
472
|
-
}
|
|
473
|
-
if (
|
|
474
|
-
subscriptionEventTypes.length > 0 &&
|
|
475
|
-
!subscriptionEventTypes.includes(String(event.event_type || '').toLowerCase())
|
|
476
|
-
) {
|
|
477
|
-
continue
|
|
478
|
-
}
|
|
479
|
-
if (subscription.topic) {
|
|
480
|
-
const haystack = `${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
|
|
481
|
-
.toLowerCase()
|
|
482
|
-
if (!haystack.includes(subscription.topic.toLowerCase())) continue
|
|
483
|
-
}
|
|
484
|
-
const eventTime = event.timestamp ? Date.parse(event.timestamp) : null
|
|
485
|
-
if (cursorTime !== null && eventTime !== null && eventTime <= cursorTime) continue
|
|
486
|
-
if (eventTime !== null && (lastEventTime === null || eventTime > lastEventTime)) {
|
|
487
|
-
lastEventTime = eventTime
|
|
488
|
-
}
|
|
489
|
-
if (seenIds.has(event.id)) continue
|
|
490
|
-
seenIds.add(event.id)
|
|
491
|
-
events.push({
|
|
492
|
-
id: event.id,
|
|
493
|
-
trace_id: event.trace_id || null,
|
|
494
|
-
event_type: event.event_type,
|
|
495
|
-
outcome: event.outcome || null,
|
|
496
|
-
timestamp: event.timestamp,
|
|
497
|
-
agent: event.agent || null,
|
|
498
|
-
payload: event.payload || null,
|
|
499
|
-
})
|
|
500
|
-
matchedSubscriptionIds.add(subscription.id)
|
|
501
|
-
if (events.length >= limit) break
|
|
502
|
-
}
|
|
503
|
-
if (lastEventTime !== null) {
|
|
504
|
-
state[subscription.id] = new Date(lastEventTime).toISOString()
|
|
505
|
-
} else if (cursor === null) {
|
|
506
|
-
state[subscription.id] = digestTime
|
|
507
|
-
}
|
|
508
|
-
if (events.length >= limit) break
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
writeDigestState(state)
|
|
512
|
-
return {
|
|
513
|
-
project,
|
|
514
|
-
agent,
|
|
515
|
-
subscriptionCount: subscriptions.length,
|
|
516
|
-
matchedSubscriptions: Array.from(matchedSubscriptionIds),
|
|
517
|
-
count: events.length,
|
|
518
|
-
events,
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function contentTokens(value) {
|
|
523
|
-
const text = String(value || '').toLowerCase()
|
|
524
|
-
const latin = text.match(/[a-z0-9]+/g) || []
|
|
525
|
-
const chars = text.match(/[\u4e00-\u9fff]/g) || []
|
|
526
|
-
const bigrams = []
|
|
527
|
-
for (let i = 0; i < chars.length - 1; i++) {
|
|
528
|
-
bigrams.push(`${chars[i]}${chars[i + 1]}`)
|
|
529
|
-
}
|
|
530
|
-
return [...latin, ...bigrams]
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
function jaccardSimilarity(a, b) {
|
|
534
|
-
const setA = new Set(contentTokens(a))
|
|
535
|
-
const setB = new Set(contentTokens(b))
|
|
536
|
-
if (setA.size === 0 || setB.size === 0) return 0
|
|
537
|
-
let intersection = 0
|
|
538
|
-
for (const token of setA) {
|
|
539
|
-
if (setB.has(token)) intersection += 1
|
|
540
|
-
}
|
|
541
|
-
const union = setA.size + setB.size - intersection
|
|
542
|
-
return union === 0 ? 0 : intersection / union
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function findDuplicateGroups(records, limit) {
|
|
546
|
-
const parent = new Map()
|
|
547
|
-
for (const record of records) parent.set(record.id, record.id)
|
|
548
|
-
const find = (id) => {
|
|
549
|
-
while (parent.get(id) !== id) {
|
|
550
|
-
parent.set(id, parent.get(parent.get(id)))
|
|
551
|
-
id = parent.get(id)
|
|
552
|
-
}
|
|
553
|
-
return id
|
|
554
|
-
}
|
|
555
|
-
const union = (a, b) => {
|
|
556
|
-
const ra = find(a)
|
|
557
|
-
const rb = find(b)
|
|
558
|
-
if (ra !== rb) parent.set(ra, rb)
|
|
559
|
-
}
|
|
560
|
-
for (let i = 0; i < records.length; i++) {
|
|
561
|
-
for (let j = i + 1; j < records.length; j++) {
|
|
562
|
-
if (
|
|
563
|
-
jaccardSimilarity(records[i].content, records[j].content) >= MEMORY_DUPLICATE_SIMILARITY
|
|
564
|
-
) {
|
|
565
|
-
union(records[i].id, records[j].id)
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
const byRoot = new Map()
|
|
570
|
-
for (const record of records) {
|
|
571
|
-
const root = find(record.id)
|
|
572
|
-
if (!byRoot.has(root)) byRoot.set(root, [])
|
|
573
|
-
byRoot.get(root).push(record)
|
|
574
|
-
}
|
|
575
|
-
return Array.from(byRoot.values())
|
|
576
|
-
.filter((group) => group.length >= 2)
|
|
577
|
-
.sort((a, b) => b.length - a.length)
|
|
578
|
-
.slice(0, limit)
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function analyzeMemory(args = {}) {
|
|
582
|
-
const project = args.project || projectName()
|
|
583
|
-
const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
|
|
584
|
-
const records = readJsonl(memoryPath).filter(
|
|
585
|
-
(record) => !project || record.project === project || !record.project,
|
|
586
|
-
)
|
|
587
|
-
const archived = records.filter((record) => record.archived === true).length
|
|
588
|
-
const activeRecords = records.filter((record) => record.archived !== true)
|
|
589
|
-
const total = activeRecords.length
|
|
590
|
-
const layerCounts = { project: 0, global: 0 }
|
|
591
|
-
for (const record of activeRecords) {
|
|
592
|
-
if (record.scope === 'global' || !record.project) layerCounts.global += 1
|
|
593
|
-
else layerCounts.project += 1
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const byKind = {}
|
|
597
|
-
const lowQuality = []
|
|
598
|
-
for (const record of activeRecords) {
|
|
599
|
-
const kind = String(record.kind || '').trim() || 'unknown'
|
|
600
|
-
byKind[kind] = (byKind[kind] || 0) + 1
|
|
601
|
-
const content = String(record.content || '').trim()
|
|
602
|
-
const issues = []
|
|
603
|
-
if (!content) issues.push('missing content')
|
|
604
|
-
else if (content.length < MEMORY_SHORT_CONTENT_MIN) issues.push('content too short')
|
|
605
|
-
if (!record.kind || !String(record.kind).trim()) issues.push('missing kind')
|
|
606
|
-
if (issues.length > 0) {
|
|
607
|
-
lowQuality.push({
|
|
608
|
-
id: record.id,
|
|
609
|
-
timestamp: record.timestamp,
|
|
610
|
-
kind: record.kind || null,
|
|
611
|
-
content: content.slice(0, 200) || null,
|
|
612
|
-
issues,
|
|
613
|
-
})
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
let duplicateGroups = []
|
|
618
|
-
let duplicatesSkipped = false
|
|
619
|
-
if (activeRecords.length > MAX_PAIRWISE_MEMORY) {
|
|
620
|
-
duplicatesSkipped = true
|
|
621
|
-
} else {
|
|
622
|
-
duplicateGroups = findDuplicateGroups(activeRecords, limit).map((group) => ({
|
|
623
|
-
size: group.length,
|
|
624
|
-
records: group.map((record) => ({
|
|
625
|
-
id: record.id,
|
|
626
|
-
timestamp: record.timestamp,
|
|
627
|
-
kind: record.kind || null,
|
|
628
|
-
content: String(record.content || '').slice(0, 200),
|
|
629
|
-
})),
|
|
630
|
-
}))
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
const issues = {
|
|
634
|
-
lowQuality: lowQuality.length,
|
|
635
|
-
duplicateGroups: duplicateGroups.length,
|
|
636
|
-
duplicatesSkipped,
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const suggestions = []
|
|
640
|
-
if (duplicateGroups.length > 0) {
|
|
641
|
-
suggestions.push(
|
|
642
|
-
`Found ${duplicateGroups.length} duplicate group(s); consider archiving or merging duplicates to keep recall precise.`,
|
|
643
|
-
)
|
|
644
|
-
}
|
|
645
|
-
if (lowQuality.length > 0) {
|
|
646
|
-
suggestions.push(
|
|
647
|
-
`${lowQuality.length} low-quality record(s) (missing kind/content or too short); consider fixing or removing them.`,
|
|
648
|
-
)
|
|
649
|
-
}
|
|
650
|
-
if (total === 0) {
|
|
651
|
-
suggestions.push('No memory records for this project yet.')
|
|
652
|
-
} else if (duplicateGroups.length === 0 && lowQuality.length === 0) {
|
|
653
|
-
suggestions.push('Memory looks healthy; no dedup or quality fixes needed.')
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
return {
|
|
657
|
-
project: project || null,
|
|
658
|
-
total,
|
|
659
|
-
archived,
|
|
660
|
-
layers: layerCounts,
|
|
661
|
-
byKind,
|
|
662
|
-
issues,
|
|
663
|
-
duplicates: duplicateGroups,
|
|
664
|
-
lowQuality: lowQuality.slice(0, limit),
|
|
665
|
-
suggestions,
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
function migrateMemory(args = {}) {
|
|
670
|
-
const idsInput = Array.isArray(args.ids) ? args.ids : []
|
|
671
|
-
const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
|
|
672
|
-
if (ids.length === 0) throw new Error('memory.migrate requires ids (array of memory record ids)')
|
|
673
|
-
const targetScope = normalizeScope(args.scope)
|
|
674
|
-
if (targetScope === 'all') throw new Error('memory.migrate scope must be project or global')
|
|
675
|
-
const project = args.project || projectName()
|
|
676
|
-
const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
|
|
677
|
-
|
|
678
|
-
const records = readJsonl(memoryPath)
|
|
679
|
-
const idSet = new Set(ids)
|
|
680
|
-
const updated = []
|
|
681
|
-
const unchanged = []
|
|
682
|
-
const notFound = [...ids]
|
|
683
|
-
for (const record of records) {
|
|
684
|
-
if (!idSet.has(record.id)) continue
|
|
685
|
-
const fromScope = record.scope || 'project'
|
|
686
|
-
const fromProject = record.project || null
|
|
687
|
-
if (fromScope === targetScope) {
|
|
688
|
-
unchanged.push(record.id)
|
|
689
|
-
} else {
|
|
690
|
-
record.scope = targetScope
|
|
691
|
-
record.project = targetScope === 'global' ? null : project
|
|
692
|
-
record.migratedAt = new Date().toISOString()
|
|
693
|
-
record.migratedFrom = fromScope === 'global' ? 'global' : fromProject || 'project'
|
|
694
|
-
if (reason) record.migrateReason = reason
|
|
695
|
-
updated.push(record.id)
|
|
696
|
-
}
|
|
697
|
-
const index = notFound.indexOf(record.id)
|
|
698
|
-
if (index >= 0) notFound.splice(index, 1)
|
|
699
|
-
}
|
|
700
|
-
writeJsonl(memoryPath, records)
|
|
701
|
-
|
|
702
|
-
return {
|
|
703
|
-
project: project || null,
|
|
704
|
-
scope: targetScope,
|
|
705
|
-
updated,
|
|
706
|
-
updatedCount: updated.length,
|
|
707
|
-
unchanged,
|
|
708
|
-
notFound,
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
function archiveMemory(args = {}) {
|
|
712
|
-
const idsInput = Array.isArray(args.ids) ? args.ids : []
|
|
713
|
-
const ids = idsInput.map((value) => String(value).trim()).filter(Boolean)
|
|
714
|
-
if (ids.length === 0) throw new Error('memory.archive requires ids (array of memory record ids)')
|
|
715
|
-
const project = args.project || projectName()
|
|
716
|
-
const restore = args.restore === true || args.restore === 'true'
|
|
717
|
-
const reason = args.reason ? String(args.reason).trim().slice(0, 200) : null
|
|
718
|
-
|
|
719
|
-
const records = readJsonl(memoryPath)
|
|
720
|
-
const idSet = new Set(ids)
|
|
721
|
-
const archived = []
|
|
722
|
-
const notFound = [...ids]
|
|
723
|
-
for (const record of records) {
|
|
724
|
-
if (!idSet.has(record.id)) continue
|
|
725
|
-
if (project && record.project && record.project !== project) continue
|
|
726
|
-
if (restore) {
|
|
727
|
-
if (record.archived === true) {
|
|
728
|
-
delete record.archived
|
|
729
|
-
delete record.archivedAt
|
|
730
|
-
delete record.archiveReason
|
|
731
|
-
archived.push(record.id)
|
|
732
|
-
}
|
|
733
|
-
} else if (record.archived !== true) {
|
|
734
|
-
record.archived = true
|
|
735
|
-
record.archivedAt = new Date().toISOString()
|
|
736
|
-
if (reason) record.archiveReason = reason
|
|
737
|
-
archived.push(record.id)
|
|
738
|
-
}
|
|
739
|
-
const index = notFound.indexOf(record.id)
|
|
740
|
-
if (index >= 0) notFound.splice(index, 1)
|
|
741
|
-
}
|
|
742
|
-
writeJsonl(memoryPath, records)
|
|
743
|
-
|
|
744
|
-
return {
|
|
745
|
-
project: project || null,
|
|
746
|
-
restored: restore,
|
|
747
|
-
archived,
|
|
748
|
-
archivedCount: archived.length,
|
|
749
|
-
notFound,
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
const FAILURE_OUTCOMES = new Set(['failure', 'error', 'aborted', 'retry'])
|
|
754
|
-
const MEMORY_DUPLICATE_SIMILARITY = 0.75
|
|
755
|
-
const MEMORY_SHORT_CONTENT_MIN = 20
|
|
756
|
-
const MAX_PAIRWISE_MEMORY = 1500
|
|
757
|
-
|
|
758
|
-
function isFailureOutcome(value) {
|
|
759
|
-
return FAILURE_OUTCOMES.has(String(value || '').toLowerCase())
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
function summarizeTrace(event) {
|
|
763
|
-
const payload = event.payload
|
|
764
|
-
if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
|
|
765
|
-
if (typeof payload.summary === 'string' && payload.summary.trim()) {
|
|
766
|
-
return payload.summary.trim().slice(0, 160)
|
|
767
|
-
}
|
|
768
|
-
if (typeof payload.tool === 'string' && payload.tool.trim()) {
|
|
769
|
-
return `tool: ${payload.tool.trim().slice(0, 120)}`
|
|
770
|
-
}
|
|
771
|
-
if (typeof payload.error === 'string' && payload.error.trim()) {
|
|
772
|
-
return payload.error.trim().slice(0, 160)
|
|
773
|
-
}
|
|
774
|
-
}
|
|
775
|
-
return String(event.event_type || 'trace').slice(0, 120)
|
|
776
|
-
}
|
|
777
|
-
|
|
778
|
-
function buildPolicyGuidance({ total, failures, risk, failureExamples, relatedMemories }) {
|
|
779
|
-
const guidance = {
|
|
780
|
-
level: 'none',
|
|
781
|
-
rationale: null,
|
|
782
|
-
saferAlternatives: [],
|
|
783
|
-
verificationSteps: [],
|
|
784
|
-
avoid: [],
|
|
785
|
-
hardGate: false,
|
|
786
|
-
}
|
|
787
|
-
if (total === 0) {
|
|
788
|
-
guidance.rationale =
|
|
789
|
-
'No historical evidence for this action yet. Treat as normal risk and record the outcome.'
|
|
790
|
-
return guidance
|
|
791
|
-
}
|
|
792
|
-
const verifiedMemories = relatedMemories.filter((record) => record.evidence)
|
|
793
|
-
const hasVerified = verifiedMemories.length > 0
|
|
794
|
-
if (risk >= 0.4 && failures >= 2) {
|
|
795
|
-
guidance.level = hasVerified ? 'actionable' : 'advisory'
|
|
796
|
-
guidance.rationale =
|
|
797
|
-
'Repeated historical failures. Prefer an alternative approach and add rollback/verification.'
|
|
798
|
-
guidance.avoid = failureExamples
|
|
799
|
-
.map((example) => example.summary)
|
|
800
|
-
.filter(Boolean)
|
|
801
|
-
.slice(0, 3)
|
|
802
|
-
} else if (risk >= 0.2 || hasVerified) {
|
|
803
|
-
guidance.level = hasVerified ? 'actionable' : 'advisory'
|
|
804
|
-
guidance.rationale = hasVerified
|
|
805
|
-
? 'Verified experience exists for this action; apply it before proceeding.'
|
|
806
|
-
: 'Moderate historical risk. Add verification before proceeding.'
|
|
807
|
-
} else {
|
|
808
|
-
guidance.level = 'advisory'
|
|
809
|
-
guidance.rationale = 'Low historical risk. Proceed with normal checks and record the outcome.'
|
|
810
|
-
}
|
|
811
|
-
if (hasVerified) {
|
|
812
|
-
guidance.saferAlternatives = verifiedMemories.slice(0, 3).map((record) => ({
|
|
813
|
-
memoryId: record.id,
|
|
814
|
-
reuseCount: record.evidence.reuseCount,
|
|
815
|
-
content: String(record.content || '').slice(0, 300),
|
|
816
|
-
}))
|
|
817
|
-
}
|
|
818
|
-
guidance.verificationSteps = [
|
|
819
|
-
'Add an explicit verification step after execution (dry-run, rollback plan, or assertion).',
|
|
820
|
-
'Record the outcome with mio.observer.ingest so the evidence base improves.',
|
|
821
|
-
]
|
|
822
|
-
return guidance
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
function policyCheck(args = {}) {
|
|
826
|
-
const action = String(args.action || '').trim()
|
|
827
|
-
if (!action) throw new Error('policy.check requires action')
|
|
828
|
-
const project = args.project || projectName()
|
|
829
|
-
const actionTokens = tokenize(action)
|
|
830
|
-
const traces = readJsonl(tracePath).filter((event) => {
|
|
831
|
-
if (project && event.project && event.project !== project) return false
|
|
832
|
-
const haystack = tokenize(
|
|
833
|
-
`${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
|
|
834
|
-
)
|
|
835
|
-
return actionTokens.some((token) => haystack.includes(token))
|
|
836
|
-
})
|
|
837
|
-
const total = traces.length
|
|
838
|
-
const failures = traces.filter((event) => isFailureOutcome(event.outcome)).length
|
|
839
|
-
const risk = total > 0 ? failures / total : null
|
|
840
|
-
const riskLevel = total === 0 ? 'unknown' : risk >= 0.4 ? 'high' : risk >= 0.2 ? 'moderate' : 'low'
|
|
841
|
-
const outcomeCounts = traces.reduce((counts, event) => {
|
|
842
|
-
const outcome = String(event.outcome || 'unknown').toLowerCase() || 'unknown'
|
|
843
|
-
counts[outcome] = (counts[outcome] || 0) + 1
|
|
844
|
-
return counts
|
|
845
|
-
}, {})
|
|
846
|
-
const failureExamples = traces
|
|
847
|
-
.filter((event) => isFailureOutcome(event.outcome))
|
|
848
|
-
.sort((a, b) => new Date(b.timestamp || 0).getTime() - new Date(a.timestamp || 0).getTime())
|
|
849
|
-
.slice(0, 3)
|
|
850
|
-
.map((event) => ({
|
|
851
|
-
trace_id: event.trace_id || event.id || null,
|
|
852
|
-
event_type: event.event_type || null,
|
|
853
|
-
outcome: event.outcome || null,
|
|
854
|
-
timestamp: event.timestamp || null,
|
|
855
|
-
agent: event.agent || null,
|
|
856
|
-
summary: summarizeTrace(event),
|
|
857
|
-
}))
|
|
858
|
-
let suggestion
|
|
859
|
-
if (total === 0) {
|
|
860
|
-
suggestion = 'No history for this action. Treat as normal risk and record the outcome.'
|
|
861
|
-
} else if (risk >= 0.4) {
|
|
862
|
-
suggestion = 'Historically risky. Prefer an alternative approach or add rollback/verification.'
|
|
863
|
-
} else if (risk >= 0.2) {
|
|
864
|
-
suggestion = 'Moderate risk. Add verification before proceeding.'
|
|
865
|
-
} else {
|
|
866
|
-
suggestion = 'Low historical risk. Proceed with normal checks.'
|
|
867
|
-
}
|
|
868
|
-
const evidence = loadEvidenceWeights()
|
|
869
|
-
const relatedMemories = readJsonl(memoryPath)
|
|
870
|
-
.filter((record) => scoreRecord(record, action, project, evidence) > 0)
|
|
871
|
-
.sort((a, b) => scoreRecord(b, action, project, evidence) - scoreRecord(a, action, project, evidence))
|
|
872
|
-
.slice(0, 3)
|
|
873
|
-
.map((record) => {
|
|
874
|
-
const ev = evidence.get(record.id)
|
|
875
|
-
return {
|
|
876
|
-
id: record.id,
|
|
877
|
-
timestamp: record.timestamp,
|
|
878
|
-
kind: record.kind,
|
|
879
|
-
content: record.content,
|
|
880
|
-
tags: record.tags,
|
|
881
|
-
evidence: ev
|
|
882
|
-
? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
|
|
883
|
-
: null,
|
|
884
|
-
}
|
|
885
|
-
})
|
|
886
|
-
return {
|
|
887
|
-
action,
|
|
888
|
-
project,
|
|
889
|
-
total,
|
|
890
|
-
failures,
|
|
891
|
-
risk: risk === null ? null : Number(risk.toFixed(3)),
|
|
892
|
-
riskLevel,
|
|
893
|
-
outcomeCounts,
|
|
894
|
-
failureExamples,
|
|
895
|
-
suggestion,
|
|
896
|
-
guidance: buildPolicyGuidance({
|
|
897
|
-
total,
|
|
898
|
-
failures,
|
|
899
|
-
risk,
|
|
900
|
-
failureExamples,
|
|
901
|
-
relatedMemories,
|
|
902
|
-
}),
|
|
903
|
-
related_memories: relatedMemories,
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
function recordExperienceReuse(args = {}) {
|
|
908
|
-
const sourceAgent = String(args.sourceAgent || '').trim()
|
|
909
|
-
const targetAgent = String(args.targetAgent || '').trim()
|
|
910
|
-
const experienceId = String(args.experienceId || '').trim()
|
|
911
|
-
if (!sourceAgent) throw new Error('experience.reuse requires sourceAgent')
|
|
912
|
-
if (!targetAgent) throw new Error('experience.reuse requires targetAgent')
|
|
913
|
-
if (!experienceId) throw new Error('experience.reuse requires experienceId')
|
|
914
|
-
|
|
915
|
-
const project = args.project || projectName()
|
|
916
|
-
const evidence = {
|
|
917
|
-
id: createId('xfer'),
|
|
918
|
-
timestamp: new Date().toISOString(),
|
|
919
|
-
sourceAgent,
|
|
920
|
-
targetAgent,
|
|
921
|
-
experienceId,
|
|
922
|
-
reuse: args.reuse === true || args.reuse === 'true',
|
|
923
|
-
behaviorChanged: args.behaviorChanged === true || args.behaviorChanged === 'true',
|
|
924
|
-
outcomeImproved: args.outcomeImproved === true || args.outcomeImproved === 'true',
|
|
925
|
-
project,
|
|
926
|
-
source: args.source || 'agent_report',
|
|
927
|
-
notes: args.notes || null,
|
|
928
|
-
}
|
|
929
|
-
appendJsonl(experienceReusePath, evidence)
|
|
930
|
-
|
|
931
|
-
const report = loadPhase0(dataDir, project)
|
|
932
|
-
return {
|
|
933
|
-
recorded: true,
|
|
934
|
-
evidence,
|
|
935
|
-
phase0: summarizePhase0(report),
|
|
936
|
-
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
function confirmExperienceReuse(args = {}) {
|
|
940
|
-
const id = String(args.id || '').trim()
|
|
941
|
-
if (!id) throw new Error('experience.confirm requires id of an auto-claimed reuse record')
|
|
942
|
-
const records = readJsonl(experienceReusePath)
|
|
943
|
-
const index = records.findIndex((record) => record && record.id === id)
|
|
944
|
-
if (index < 0) {
|
|
945
|
-
throw new Error(`No experience reuse record found with id ${id}`)
|
|
946
|
-
}
|
|
947
|
-
const record = records[index]
|
|
948
|
-
if (record.source !== 'auto_claim') {
|
|
949
|
-
throw new Error('experience.confirm only applies to source=auto_claim reuse records')
|
|
950
|
-
}
|
|
951
|
-
const outcomeImproved =
|
|
952
|
-
args.outcomeImproved === undefined || args.outcomeImproved === null
|
|
953
|
-
? record.outcomeImproved === true
|
|
954
|
-
: args.outcomeImproved === true || args.outcomeImproved === 'true'
|
|
955
|
-
const updated = {
|
|
956
|
-
...record,
|
|
957
|
-
behaviorChanged: true,
|
|
958
|
-
outcomeImproved,
|
|
959
|
-
confirmed: true,
|
|
960
|
-
confirmedAt: new Date().toISOString(),
|
|
961
|
-
confirmedBy: String(args.confirmedBy || 'agent').trim() || 'agent',
|
|
962
|
-
notes: args.notes ? String(args.notes).trim() : record.notes || null,
|
|
963
|
-
}
|
|
964
|
-
records[index] = updated
|
|
965
|
-
writeJsonl(experienceReusePath, records)
|
|
966
|
-
|
|
967
|
-
const report = loadPhase0(dataDir, updated.project)
|
|
968
|
-
return {
|
|
969
|
-
confirmed: true,
|
|
970
|
-
evidence: updated,
|
|
971
|
-
phase0: summarizePhase0(report),
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
function listExperienceReuse(args = {}) {
|
|
976
|
-
const project = args.project || projectName()
|
|
977
|
-
const status = String(args.status || 'all').trim().toLowerCase()
|
|
978
|
-
const targetAgent = String(args.targetAgent || '').trim().toLowerCase()
|
|
979
|
-
const limit = Math.min(Math.max(Number(args.limit) || 20, 1), 100)
|
|
980
|
-
|
|
981
|
-
let records = readJsonl(experienceReusePath)
|
|
982
|
-
if (project) records = records.filter((record) => record.project === project)
|
|
983
|
-
if (targetAgent) {
|
|
984
|
-
records = records.filter(
|
|
985
|
-
(record) => String(record.targetAgent || '').toLowerCase() === targetAgent,
|
|
986
|
-
)
|
|
987
|
-
}
|
|
988
|
-
const matcher = REUSE_STATUS_FILTERS[status]
|
|
989
|
-
if (matcher) records = records.filter(matcher)
|
|
990
|
-
records.sort((a, b) => {
|
|
991
|
-
const at = new Date(a.timestamp || 0).getTime()
|
|
992
|
-
const bt = new Date(b.timestamp || 0).getTime()
|
|
993
|
-
return bt - at
|
|
994
|
-
})
|
|
995
|
-
|
|
996
|
-
const visible = records.slice(0, limit).map((record) => ({
|
|
997
|
-
id: record.id,
|
|
998
|
-
source: record.source,
|
|
999
|
-
sourceAgent: record.sourceAgent,
|
|
1000
|
-
targetAgent: record.targetAgent,
|
|
1001
|
-
experienceId: record.experienceId,
|
|
1002
|
-
reuse: record.reuse === true || record.reuse === 'true',
|
|
1003
|
-
behaviorChanged: record.behaviorChanged === true || record.behaviorChanged === 'true',
|
|
1004
|
-
outcomeImproved: record.outcomeImproved === true || record.outcomeImproved === 'true',
|
|
1005
|
-
confirmed: record.confirmed === true,
|
|
1006
|
-
confirmedBy: record.confirmedBy || null,
|
|
1007
|
-
traceId: record.traceId || null,
|
|
1008
|
-
timestamp: record.timestamp,
|
|
1009
|
-
project: record.project,
|
|
1010
|
-
notes:
|
|
1011
|
-
typeof record.notes === 'string' && record.notes ? record.notes.slice(0, 300) : null,
|
|
1012
|
-
}))
|
|
1013
|
-
|
|
1014
|
-
return {
|
|
1015
|
-
project: project || null,
|
|
1016
|
-
status,
|
|
1017
|
-
count: visible.length,
|
|
1018
|
-
total: records.length,
|
|
1019
|
-
records: visible,
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
function registerAgent(args = {}) {
|
|
1024
|
-
const agentId = String(args.agentId || '').trim()
|
|
1025
|
-
if (!agentId) throw new Error('agent.register requires a non-empty agentId')
|
|
1026
|
-
const project = args.project || projectName()
|
|
1027
|
-
const hostType = args.hostType || 'mcp'
|
|
1028
|
-
const capabilities = Array.isArray(args.capabilities) ? args.capabilities : []
|
|
1029
|
-
const now = new Date().toISOString()
|
|
1030
|
-
const agents = readJsonl(agentsPath)
|
|
1031
|
-
const existing = agents.find((a) => a.agentId === agentId && a.project === project)
|
|
1032
|
-
let result
|
|
1033
|
-
if (existing) {
|
|
1034
|
-
existing.lastSeenAt = now
|
|
1035
|
-
existing.sessionCount = (existing.sessionCount || 1) + 1
|
|
1036
|
-
existing.hostType = hostType
|
|
1037
|
-
if (capabilities.length > 0) existing.capabilities = capabilities
|
|
1038
|
-
result = existing
|
|
1039
|
-
} else {
|
|
1040
|
-
result = {
|
|
1041
|
-
id: createId('agent'),
|
|
1042
|
-
agentId,
|
|
1043
|
-
hostType,
|
|
1044
|
-
capabilities,
|
|
1045
|
-
registeredAt: now,
|
|
1046
|
-
lastSeenAt: now,
|
|
1047
|
-
sessionCount: 1,
|
|
1048
|
-
project,
|
|
1049
|
-
}
|
|
1050
|
-
agents.push(result)
|
|
1051
|
-
}
|
|
1052
|
-
writeJsonl(agentsPath, agents)
|
|
1053
|
-
return { id: result.id, agentId: result.agentId, registered: !existing, sessionCount: result.sessionCount, hostType: result.hostType, capabilities: result.capabilities }
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
|
-
function listAgents(args = {}) {
|
|
1057
|
-
const project = args.project || null
|
|
1058
|
-
let agents = readJsonl(agentsPath)
|
|
1059
|
-
if (project) agents = agents.filter((a) => a.project === project)
|
|
1060
|
-
return {
|
|
1061
|
-
count: agents.length,
|
|
1062
|
-
agents: agents.map((a) => ({
|
|
1063
|
-
id: a.id, agentId: a.agentId, hostType: a.hostType,
|
|
1064
|
-
capabilities: a.capabilities, registeredAt: a.registeredAt,
|
|
1065
|
-
lastSeenAt: a.lastSeenAt, sessionCount: a.sessionCount,
|
|
1066
|
-
taskCount: a.taskCount || 0, successCount: a.successCount || 0, failureCount: a.failureCount || 0,
|
|
1067
|
-
project: a.project,
|
|
1068
|
-
})),
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
function reportAgent(args = {}) {
|
|
1073
|
-
const targetAgent = args.agentId || null
|
|
1074
|
-
const project = args.project || projectName()
|
|
1075
|
-
const agents = readJsonl(agentsPath)
|
|
1076
|
-
const filtered = agents.filter((a) =>
|
|
1077
|
-
(!targetAgent || a.agentId === targetAgent) && (!project || a.project === project)
|
|
1078
|
-
)
|
|
1079
|
-
const traces = readJsonl(tracePath)
|
|
1080
|
-
const memories = readJsonl(memoryPath)
|
|
1081
|
-
const reuses = readJsonl(experienceReusePath)
|
|
1082
|
-
const reports = filtered.map((agent) => {
|
|
1083
|
-
const agentTraces = traces.filter((t) => t.agent === agent.agentId)
|
|
1084
|
-
const outcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
|
|
1085
|
-
const successes = outcomes.filter((t) => t.outcome === 'success').length
|
|
1086
|
-
const failures = outcomes.filter((t) => t.outcome === 'failure').length
|
|
1087
|
-
const agentMemories = memories.filter((m) => m.source === agent.agentId)
|
|
1088
|
-
const agentReuses = reuses.filter((r) => r.sourceAgent === agent.agentId || r.targetAgent === agent.agentId)
|
|
1089
|
-
const verified = agentReuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
|
|
1090
|
-
const regTasks = agent.taskCount || 0
|
|
1091
|
-
const regSuccess = agent.successCount || 0
|
|
1092
|
-
const regFail = agent.failureCount || 0
|
|
1093
|
-
const taskTotal = regTasks > 0 ? regTasks : outcomes.length
|
|
1094
|
-
const taskSuccess = regTasks > 0 ? regSuccess : successes
|
|
1095
|
-
const taskFail = regTasks > 0 ? regFail : failures
|
|
1096
|
-
return {
|
|
1097
|
-
agentId: agent.agentId,
|
|
1098
|
-
hostType: agent.hostType,
|
|
1099
|
-
sessionCount: agent.sessionCount,
|
|
1100
|
-
registeredAt: agent.registeredAt,
|
|
1101
|
-
lastSeenAt: agent.lastSeenAt,
|
|
1102
|
-
taskOutcomes: { total: taskTotal, success: taskSuccess, failure: taskFail, successRate: taskTotal > 0 ? Math.round((taskSuccess / taskTotal) * 100) : 0 },
|
|
1103
|
-
memories: agentMemories.length,
|
|
1104
|
-
experienceReuses: { total: agentReuses.length, verified: verified.length },
|
|
1105
|
-
active: (Date.now() - new Date(agent.lastSeenAt).getTime()) < 24 * 60 * 60 * 1000,
|
|
1106
|
-
}
|
|
1107
|
-
})
|
|
1108
|
-
return { project, count: reports.length, reports }
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function routeTask(args = {}) {
|
|
1112
|
-
const task = String(args.task || '').trim()
|
|
1113
|
-
if (!task) throw new Error('task.route requires a non-empty task')
|
|
1114
|
-
const project = args.project || projectName()
|
|
1115
|
-
const scope = normalizeScope(args.scope)
|
|
1116
|
-
const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 10)
|
|
1117
|
-
|
|
1118
|
-
const memories = readJsonl(memoryPath).filter((record) => record.archived !== true)
|
|
1119
|
-
const memoryById = new Map(memories.map((record) => [record.id, record]))
|
|
1120
|
-
const evidence = loadEvidenceWeights()
|
|
1121
|
-
|
|
1122
|
-
// Match verified reuse experiences whose source memory is relevant to the task.
|
|
1123
|
-
const routeByMemory = new Map()
|
|
1124
|
-
for (const record of readJsonl(experienceReusePath)) {
|
|
1125
|
-
if (!REUSE_STATUS_FILTERS.verified(record)) continue
|
|
1126
|
-
const memory = memoryById.get(record.experienceId)
|
|
1127
|
-
if (!memory || !matchesProjectScope(memory, project, scope)) continue
|
|
1128
|
-
const score = scoreRecord(memory, task, project)
|
|
1129
|
-
if (score <= 0) continue
|
|
1130
|
-
let entry = routeByMemory.get(record.experienceId)
|
|
1131
|
-
if (!entry) {
|
|
1132
|
-
entry = {
|
|
1133
|
-
experienceId: record.experienceId,
|
|
1134
|
-
memory,
|
|
1135
|
-
weight: 0,
|
|
1136
|
-
confirmed: false,
|
|
1137
|
-
sourceAgents: new Set(),
|
|
1138
|
-
targetAgents: new Set(),
|
|
1139
|
-
reuseCount: 0,
|
|
1140
|
-
}
|
|
1141
|
-
routeByMemory.set(record.experienceId, entry)
|
|
1142
|
-
}
|
|
1143
|
-
entry.reuseCount += 1
|
|
1144
|
-
if (record.confirmed === true) entry.confirmed = true
|
|
1145
|
-
if (record.sourceAgent) entry.sourceAgents.add(record.sourceAgent)
|
|
1146
|
-
if (record.targetAgent) entry.targetAgents.add(record.targetAgent)
|
|
1147
|
-
entry.weight = Math.max(entry.weight, score + (entry.confirmed ? 2 : 1.5))
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
|
-
const routes = [...routeByMemory.values()]
|
|
1151
|
-
.sort((a, b) => b.weight - a.weight)
|
|
1152
|
-
.slice(0, limit)
|
|
1153
|
-
.map((entry) => ({
|
|
1154
|
-
experienceId: entry.experienceId,
|
|
1155
|
-
reuseCount: entry.reuseCount,
|
|
1156
|
-
confirmed: entry.confirmed,
|
|
1157
|
-
sourceAgents: [...entry.sourceAgents].filter(Boolean),
|
|
1158
|
-
targetAgents: [...entry.targetAgents].filter(Boolean),
|
|
1159
|
-
score: Number(entry.weight.toFixed(2)),
|
|
1160
|
-
memory: {
|
|
1161
|
-
id: entry.memory.id,
|
|
1162
|
-
timestamp: entry.memory.timestamp,
|
|
1163
|
-
kind: entry.memory.kind,
|
|
1164
|
-
content: entry.memory.content,
|
|
1165
|
-
tags: entry.memory.tags || [],
|
|
1166
|
-
project: entry.memory.project || null,
|
|
1167
|
-
scope: entry.memory.scope || 'project',
|
|
1168
|
-
},
|
|
1169
|
-
}))
|
|
203
|
+
// policy.check reuses the memory store's tokenizer and scoring so its risk
|
|
204
|
+
// evidence and related-memory ranking match mio.memory.query exactly.
|
|
205
|
+
const policyStore = createPolicyStore({
|
|
206
|
+
dataDir,
|
|
207
|
+
projectName,
|
|
208
|
+
memoryStore,
|
|
209
|
+
tracePath,
|
|
210
|
+
memoryPath,
|
|
211
|
+
})
|
|
212
|
+
const { policyCheck } = policyStore
|
|
1170
213
|
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
(scope === 'all' && isGlobalRecord(record) ? 0.5 : 0)
|
|
1176
|
-
const relatedMemories = memories
|
|
1177
|
-
.filter((record) => !routedIds.has(record.id))
|
|
1178
|
-
.filter((record) => matchesProjectScope(record, project, scope))
|
|
1179
|
-
.filter((record) => scoreRelated(record) > 0)
|
|
1180
|
-
.sort((a, b) => scoreRelated(b) - scoreRelated(a))
|
|
1181
|
-
.slice(0, Math.max(limit, 5))
|
|
1182
|
-
.map((record) => {
|
|
1183
|
-
const ev = evidence.get(record.id)
|
|
1184
|
-
return {
|
|
1185
|
-
id: record.id,
|
|
1186
|
-
timestamp: record.timestamp,
|
|
1187
|
-
kind: record.kind,
|
|
1188
|
-
content: record.content,
|
|
1189
|
-
tags: record.tags || [],
|
|
1190
|
-
project: record.project || null,
|
|
1191
|
-
scope: record.scope || 'project',
|
|
1192
|
-
evidence: ev
|
|
1193
|
-
? { reuseCount: ev.reuseCount, confirmedCount: ev.confirmedCount }
|
|
1194
|
-
: null,
|
|
1195
|
-
}
|
|
1196
|
-
})
|
|
214
|
+
// agent.list / register / report delegate to the shared store so the CLI
|
|
215
|
+
// (mio agents list / report) and this MCP tool report identical telemetry.
|
|
216
|
+
const agentStore = createAgentStore({ dataDir, projectName })
|
|
217
|
+
const { listAgents, reportAgent, registerAgent } = agentStore
|
|
1197
218
|
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
expiresAt: now + REUSE_MATCH_WINDOW_MS,
|
|
1210
|
-
})
|
|
1211
|
-
persistRecentQueries(entries)
|
|
1212
|
-
}
|
|
219
|
+
// task.route delegates to the shared store so the CLI (`mio task route`) and
|
|
220
|
+
// this MCP tool route identically. The MCP keeps feeding the query log
|
|
221
|
+
// (auto-claim depends on it); the CLI passes persistQuery: false.
|
|
222
|
+
const taskStore = createTaskStore({
|
|
223
|
+
dataDir,
|
|
224
|
+
projectName,
|
|
225
|
+
memoryStore,
|
|
226
|
+
queryLog,
|
|
227
|
+
agentId: runtimeAgentId,
|
|
228
|
+
})
|
|
229
|
+
const { routeTask, autoClaimExperienceReuse, recordTaskOutcome, ingestObservation } = taskStore
|
|
1213
230
|
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
'No verified experience matches yet. Consult the related memories, then record the outcome so this task can seed future routes.'
|
|
1221
|
-
} else {
|
|
1222
|
-
suggestion =
|
|
1223
|
-
'No matching experience or memory found. Proceed with care and record the outcome to grow the experience base.'
|
|
1224
|
-
}
|
|
231
|
+
const subscriptionStore = createSubscriptionStore({
|
|
232
|
+
dataDir,
|
|
233
|
+
projectName,
|
|
234
|
+
agentId: runtimeAgentId,
|
|
235
|
+
})
|
|
236
|
+
const { subscribe: subscribeObserver, digest: observerDigest } = subscriptionStore
|
|
1225
237
|
|
|
1226
|
-
return {
|
|
1227
|
-
task,
|
|
1228
|
-
project,
|
|
1229
|
-
scope,
|
|
1230
|
-
count: routes.length,
|
|
1231
|
-
routes,
|
|
1232
|
-
relatedMemories,
|
|
1233
|
-
summary: {
|
|
1234
|
-
verifiedRoutes: routes.length,
|
|
1235
|
-
relatedMemories: relatedMemories.length,
|
|
1236
|
-
suggestion,
|
|
1237
|
-
},
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
238
|
function phase0Report(args = {}) {
|
|
1241
239
|
const project = args.project || projectName()
|
|
1242
240
|
const report = loadPhase0(dataDir, project)
|
|
@@ -1285,6 +283,34 @@ const TOOLS = [
|
|
|
1285
283
|
required: ['ids'],
|
|
1286
284
|
},
|
|
1287
285
|
},
|
|
286
|
+
{
|
|
287
|
+
name: 'mio.memory.forget',
|
|
288
|
+
description: 'Permanently delete memory records by id. This is the only irreversible memory operation: archive is reversible, and forget removes the record outright. An audit entry (id, project, timestamp, reason, kind, length, excerpt) is written to memory-forget-audit.jsonl before the delete, so a removal is always reconstructable. Prefer archive unless the record must actually be gone.',
|
|
289
|
+
inputSchema: {
|
|
290
|
+
type: 'object',
|
|
291
|
+
properties: {
|
|
292
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to permanently delete.' },
|
|
293
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
294
|
+
reason: { type: 'string', description: 'Why the record is being forgotten. Recorded in the audit entry.' },
|
|
295
|
+
},
|
|
296
|
+
required: ['ids'],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
name: 'mio.memory.merge',
|
|
301
|
+
description: 'Merge near-duplicate memory records into one survivor: the survivor gains a supersedes list and the others are archived. Refuses to merge records whose content differs unless keep and allowDivergent are both given, because concatenating divergent bodies corrupts them. Reversible via mio.memory.archive with restore.',
|
|
302
|
+
inputSchema: {
|
|
303
|
+
type: 'object',
|
|
304
|
+
properties: {
|
|
305
|
+
ids: { type: 'array', items: { type: 'string' }, description: 'Memory record ids to merge (at least 2).' },
|
|
306
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
307
|
+
keep: { type: 'string', description: 'Id of the record whose content survives. Defaults to the newest record in the set.' },
|
|
308
|
+
allowDivergent: { type: 'boolean', description: 'When true, allow merging records whose content differs. Requires keep so the surviving body is explicit.' },
|
|
309
|
+
reason: { type: 'string', description: 'Optional merge reason.' },
|
|
310
|
+
},
|
|
311
|
+
required: ['ids'],
|
|
312
|
+
},
|
|
313
|
+
},
|
|
1288
314
|
{
|
|
1289
315
|
name: 'mio.memory.migrate',
|
|
1290
316
|
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.',
|
|
@@ -1468,7 +494,7 @@ const TOOLS = [
|
|
|
1468
494
|
},
|
|
1469
495
|
{
|
|
1470
496
|
name: 'mio.task.route',
|
|
1471
|
-
description: 'Route an incoming task to the most relevant verified experiences and related memories,
|
|
497
|
+
description: 'Route an incoming task to the most relevant verified experiences and related memories, and include an agent-health signal (from the latest digest) saying which agents to prefer or avoid. Turns recall plus observed performance into decision influence (P1).',
|
|
1472
498
|
inputSchema: {
|
|
1473
499
|
type: 'object',
|
|
1474
500
|
properties: {
|
|
@@ -1695,7 +721,7 @@ const TOOLS = [
|
|
|
1695
721
|
|
|
1696
722
|
// ═══ Insight Self-Observation ═══
|
|
1697
723
|
|
|
1698
|
-
...(
|
|
724
|
+
...(isInsightAvailable() ? [{
|
|
1699
725
|
name: 'mio.insight.status',
|
|
1700
726
|
description: 'Get insight engine status: total insights, unreported count, cooldown.',
|
|
1701
727
|
inputSchema: { type: 'object', properties: {} },
|
|
@@ -1739,7 +765,7 @@ const TOOLS = [
|
|
|
1739
765
|
|
|
1740
766
|
// ═══ Observer Research Pipeline ═══
|
|
1741
767
|
|
|
1742
|
-
...(
|
|
768
|
+
...(isObserverAvailable() ? [{
|
|
1743
769
|
name: 'mio.observer.world_model',
|
|
1744
770
|
description: 'Query the Observer world model: entities, events, trends, narratives, uncertainties, and relations.',
|
|
1745
771
|
inputSchema: {
|
|
@@ -1843,204 +869,64 @@ const TOOLS = [
|
|
|
1843
869
|
]
|
|
1844
870
|
|
|
1845
871
|
// ═══ Insight Self-Observation Handlers ═══
|
|
872
|
+
// Thin delegates to ../insight-store.js: the store owns the implementation so
|
|
873
|
+
// `mio insight` (CLI) and mio.insight.* (MCP) cannot drift.
|
|
1846
874
|
|
|
1847
875
|
function insightStatus() {
|
|
1848
|
-
|
|
1849
|
-
const all = insightStore.getAll()
|
|
1850
|
-
const unreported = insightStore.getUnreported()
|
|
1851
|
-
return {
|
|
1852
|
-
total: all.length,
|
|
1853
|
-
unreported: unreported.length,
|
|
1854
|
-
reported: all.length - unreported.length,
|
|
1855
|
-
highValue: insightStore.getHighValueUnreported(50, 0.7).length,
|
|
1856
|
-
}
|
|
876
|
+
return insightStore.status()
|
|
1857
877
|
}
|
|
1858
878
|
|
|
1859
879
|
function insightList(args = {}) {
|
|
1860
|
-
|
|
1861
|
-
let insights = insightStore.getAll()
|
|
1862
|
-
if (args.unreported) insights = insightStore.getUnreported()
|
|
1863
|
-
if (args.minScore) insights = insights.filter(i => i.score >= args.minScore)
|
|
1864
|
-
if (args.detector) insights = insights.filter(i => i.detector === args.detector)
|
|
1865
|
-
if (args.limit) insights = insights.slice(-args.limit)
|
|
1866
|
-
return insights
|
|
880
|
+
return insightStore.list(args)
|
|
1867
881
|
}
|
|
1868
882
|
|
|
1869
883
|
function insightMarkReported(args = {}) {
|
|
1870
|
-
|
|
1871
|
-
if (!args.ids || !Array.isArray(args.ids)) throw new Error('ids array required')
|
|
1872
|
-
for (const id of args.ids) insightStore.markReported(id)
|
|
1873
|
-
return { marked: args.ids.length }
|
|
884
|
+
return insightStore.markReported(args)
|
|
1874
885
|
}
|
|
1875
886
|
|
|
1876
887
|
async function insightGenerate(args = {}) {
|
|
1877
|
-
|
|
1878
|
-
const generator = new InsightGenerator({ chatJson })
|
|
1879
|
-
const ctx = {
|
|
1880
|
-
memoryEntries: (args.memories || []).map(m => ({
|
|
1881
|
-
type: m.kind || m.type || 'note',
|
|
1882
|
-
content: m.content || '',
|
|
1883
|
-
createdAt: m.createdAt || m.timestamp || Date.now(),
|
|
1884
|
-
})),
|
|
1885
|
-
summaries: args.summaries || [],
|
|
1886
|
-
interactionCount: 0,
|
|
1887
|
-
plans: (args.plans || []).map(p => ({
|
|
1888
|
-
title: p.title || p.name || '',
|
|
1889
|
-
status: p.status || 'unknown',
|
|
1890
|
-
updatedAt: p.updatedAt || Date.now(),
|
|
1891
|
-
steps: p.steps || [],
|
|
1892
|
-
createdAt: p.createdAt || Date.now(),
|
|
1893
|
-
})),
|
|
1894
|
-
eventCount: 0,
|
|
1895
|
-
}
|
|
1896
|
-
const insights = await generator.generate(ctx)
|
|
1897
|
-
if (insightStore) {
|
|
1898
|
-
for (const i of insights) insightStore.addMany([i])
|
|
1899
|
-
}
|
|
1900
|
-
return { generated: insights.length, insights }
|
|
888
|
+
return insightStore.generate(args)
|
|
1901
889
|
}
|
|
1902
890
|
|
|
1903
891
|
// ═══ Observer Research Pipeline Handlers ═══
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
return new ObserverStore(baseDir || path.join(process.cwd(), '.local', 'observer'))
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
|
-
function getObserverBaseDir(args) {
|
|
1911
|
-
return args.baseDir || path.join(process.cwd(), '.local', 'observer')
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
function readJsonSafe(filePath) {
|
|
1915
|
-
if (!fs.existsSync(filePath)) return null
|
|
1916
|
-
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')) } catch { return null }
|
|
1917
|
-
}
|
|
1918
|
-
|
|
1919
|
-
function readJsonlSafe(filePath) {
|
|
1920
|
-
if (!fs.existsSync(filePath)) return []
|
|
1921
|
-
try {
|
|
1922
|
-
return fs.readFileSync(filePath, 'utf8').trim().split('\n').filter(Boolean)
|
|
1923
|
-
.map(l => { try { return JSON.parse(l) } catch { return null } }).filter(Boolean)
|
|
1924
|
-
} catch { return [] }
|
|
1925
|
-
}
|
|
892
|
+
// Thin delegates to ../observer-store.js (same rationale as above).
|
|
893
|
+
// readJsonSafe / readJsonlSafe / getObserverBaseDir used to live here; they now
|
|
894
|
+
// live with the store, which is their only consumer.
|
|
1926
895
|
|
|
1927
896
|
function observerWorldModel(args = {}) {
|
|
1928
|
-
|
|
1929
|
-
const wmDir = path.join(baseDir, 'world_model')
|
|
1930
|
-
return {
|
|
1931
|
-
entities: readJsonSafe(path.join(wmDir, 'entities.json')) || [],
|
|
1932
|
-
events: readJsonSafe(path.join(wmDir, 'events.json')) || [],
|
|
1933
|
-
trends: readJsonSafe(path.join(wmDir, 'trends.json')) || [],
|
|
1934
|
-
narratives: readJsonSafe(path.join(wmDir, 'narratives.json')) || [],
|
|
1935
|
-
}
|
|
897
|
+
return observerStore.worldModel(args)
|
|
1936
898
|
}
|
|
1937
899
|
|
|
1938
900
|
function observerTrends(args = {}) {
|
|
1939
|
-
|
|
1940
|
-
const trendsDir = path.join(baseDir, 'trends')
|
|
1941
|
-
if (!fs.existsSync(trendsDir)) return []
|
|
1942
|
-
if (args.date) {
|
|
1943
|
-
const report = readJsonSafe(path.join(trendsDir, `${args.date}.json`))
|
|
1944
|
-
return report ? [report] : []
|
|
1945
|
-
}
|
|
1946
|
-
const files = fs.readdirSync(trendsDir).filter(f => f.endsWith('.json')).sort().reverse()
|
|
1947
|
-
return files.slice(0, args.limit || 7).map(f => readJsonSafe(path.join(trendsDir, f))).filter(Boolean)
|
|
901
|
+
return observerStore.trends(args)
|
|
1948
902
|
}
|
|
1949
903
|
|
|
1950
904
|
function observerResearch(args = {}) {
|
|
1951
|
-
|
|
1952
|
-
const researchDir = path.join(baseDir, 'research')
|
|
1953
|
-
if (!fs.existsSync(researchDir)) return []
|
|
1954
|
-
const files = fs.readdirSync(researchDir).filter(f => f.endsWith('.json')).sort().reverse()
|
|
1955
|
-
return files.slice(0, args.limit || 10).map(f => readJsonSafe(path.join(researchDir, f))).filter(Boolean)
|
|
905
|
+
return observerStore.research(args)
|
|
1956
906
|
}
|
|
1957
907
|
|
|
1958
908
|
function observerInsights(args = {}) {
|
|
1959
|
-
|
|
1960
|
-
const insightsDir = path.join(baseDir, 'insights')
|
|
1961
|
-
if (!fs.existsSync(insightsDir)) return []
|
|
1962
|
-
const files = fs.readdirSync(insightsDir).filter(f => f.endsWith('.json')).sort().reverse()
|
|
1963
|
-
return files.slice(0, args.limit || 20).map(f => readJsonSafe(path.join(insightsDir, f))).filter(Boolean)
|
|
909
|
+
return observerStore.insights(args)
|
|
1964
910
|
}
|
|
1965
911
|
|
|
1966
912
|
function observerStatus(args = {}) {
|
|
1967
|
-
|
|
1968
|
-
const dirs = ['observations', 'trends', 'topics', 'research', 'insights', 'world_model', 'essays']
|
|
1969
|
-
const status = {}
|
|
1970
|
-
for (const d of dirs) {
|
|
1971
|
-
const dir = path.join(baseDir, d)
|
|
1972
|
-
if (fs.existsSync(dir)) {
|
|
1973
|
-
status[d] = fs.readdirSync(dir).filter(f => !f.startsWith('.')).length
|
|
1974
|
-
} else {
|
|
1975
|
-
status[d] = 0
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
return status
|
|
913
|
+
return observerStore.status(args)
|
|
1979
914
|
}
|
|
1980
915
|
|
|
1981
916
|
function observerCollect(args = {}) {
|
|
1982
|
-
|
|
1983
|
-
const baseDir = getObserverBaseDir(args)
|
|
1984
|
-
const service = new ObserverService({ baseDir, collectorConfig: {} })
|
|
1985
|
-
const sources = args.sources || ['bilibili', 'hackernews', 'github', 'douyin', 'rss']
|
|
1986
|
-
const allObs = []
|
|
1987
|
-
for (const src of sources) {
|
|
1988
|
-
try {
|
|
1989
|
-
const collectFn = service.collectBySource ? service.collectBySource : service.collect
|
|
1990
|
-
if (collectFn) {
|
|
1991
|
-
const result = collectFn.call(service, [src], args.keywords || [], args.limit || 20)
|
|
1992
|
-
if (Array.isArray(result)) allObs.push(...result)
|
|
1993
|
-
else if (result && typeof result === 'object') {
|
|
1994
|
-
for (const [key, items] of Object.entries(result)) allObs.push(...items)
|
|
1995
|
-
}
|
|
1996
|
-
}
|
|
1997
|
-
} catch (e) {
|
|
1998
|
-
allObs.push({ source: src, error: e.message })
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
return { collected: allObs.length, observations: allObs }
|
|
917
|
+
return observerStore.collect(args)
|
|
2002
918
|
}
|
|
2003
919
|
|
|
2004
920
|
function observerFerment(args = {}) {
|
|
2005
|
-
|
|
2006
|
-
const baseDir = getObserverBaseDir(args)
|
|
2007
|
-
const service = new ObserverService({ baseDir, collectorConfig: {} })
|
|
2008
|
-
const session = args.session || 'afternoon'
|
|
2009
|
-
try {
|
|
2010
|
-
const fermentation = service.getFermentation()
|
|
2011
|
-
if (fermentation && fermentation.ferment) return fermentation.ferment(session)
|
|
2012
|
-
return { status: 'fermentation engine available but no ferment method' }
|
|
2013
|
-
} catch (e) {
|
|
2014
|
-
return { error: e.message }
|
|
2015
|
-
}
|
|
921
|
+
return observerStore.ferment(args)
|
|
2016
922
|
}
|
|
2017
923
|
|
|
2018
924
|
function observerEssays(args = {}) {
|
|
2019
|
-
|
|
2020
|
-
const type = args.type || 'published'
|
|
2021
|
-
const essaysDir = path.join(baseDir, 'essays', type)
|
|
2022
|
-
if (!fs.existsSync(essaysDir)) return []
|
|
2023
|
-
const files = fs.readdirSync(essaysDir).filter(f => f.endsWith('.md')).sort().reverse()
|
|
2024
|
-
return files.slice(0, args.limit || 10).map(f => {
|
|
2025
|
-
const content = fs.readFileSync(path.join(essaysDir, f), 'utf8')
|
|
2026
|
-
return { file: f, type, content, created: fs.statSync(path.join(essaysDir, f)).birthtime.toISOString() }
|
|
2027
|
-
})
|
|
925
|
+
return observerStore.essays(args)
|
|
2028
926
|
}
|
|
2029
927
|
|
|
2030
928
|
function observerDag(args = {}) {
|
|
2031
|
-
|
|
2032
|
-
const days = args.days || 7
|
|
2033
|
-
const summaries = []
|
|
2034
|
-
const todayDate = new Date()
|
|
2035
|
-
for (let i = 0; i < days; i++) {
|
|
2036
|
-
const d = new Date(todayDate)
|
|
2037
|
-
d.setDate(d.getDate() - i)
|
|
2038
|
-
const dateStr = d.toISOString().slice(0, 10)
|
|
2039
|
-
const filePath = path.join(baseDir, 'summaries', `${dateStr}.jsonl`)
|
|
2040
|
-
const items = readJsonlSafe(filePath)
|
|
2041
|
-
if (items.length) summaries.push({ date: dateStr, summaries: items })
|
|
2042
|
-
}
|
|
2043
|
-
return { summaries, summaryCount: summaries.reduce((a, s) => a + s.summaries.length, 0) }
|
|
929
|
+
return observerStore.dag(args)
|
|
2044
930
|
}
|
|
2045
931
|
|
|
2046
932
|
async function callTool(name, args = {}) {
|
|
@@ -2051,6 +937,10 @@ async function callTool(name, args = {}) {
|
|
|
2051
937
|
return recordMemory(args)
|
|
2052
938
|
case 'mio.memory.archive':
|
|
2053
939
|
return archiveMemory(args)
|
|
940
|
+
case 'mio.memory.forget':
|
|
941
|
+
return forgetMemory(args)
|
|
942
|
+
case 'mio.memory.merge':
|
|
943
|
+
return mergeMemory(args)
|
|
2054
944
|
case 'mio.memory.migrate':
|
|
2055
945
|
return migrateMemory(args)
|
|
2056
946
|
case 'mio.observer.ingest':
|
|
@@ -2219,241 +1109,12 @@ rl.on('close', () => {
|
|
|
2219
1109
|
})
|
|
2220
1110
|
|
|
2221
1111
|
|
|
2222
|
-
|
|
2223
|
-
const project = args.project || projectName()
|
|
2224
|
-
const period = String(args.period || 'all').trim()
|
|
2225
|
-
const now = Date.now()
|
|
2226
|
-
const periodMs = period === '24h' ? 86400000 : period === '7d' ? 604800000 : period === '30d' ? 2592000000 : Infinity
|
|
2227
|
-
const cutoff = periodMs === Infinity ? 0 : now - periodMs
|
|
2228
|
-
|
|
2229
|
-
const memories = readJsonl(memoryPath).filter((m) => !project || m.project === project || !m.project)
|
|
2230
|
-
const traces = readJsonl(tracePath).filter((t) => !project || t.project === project)
|
|
2231
|
-
const reuses = readJsonl(experienceReusePath).filter((r) => !project || r.project === project)
|
|
2232
|
-
const agents = readJsonl(agentsPath).filter((a) => !project || a.project === project)
|
|
2233
|
-
const queries = readJsonl(queryPath).filter((q) => !project || q.project === project)
|
|
2234
|
-
|
|
2235
|
-
const filteredTraces = cutoff ? traces.filter((t) => new Date(t.timestamp).getTime() >= cutoff) : traces
|
|
2236
|
-
const filteredMemories = cutoff ? memories.filter((m) => new Date(m.timestamp).getTime() >= cutoff) : memories
|
|
2237
|
-
|
|
2238
|
-
// Ecosystem summary
|
|
2239
|
-
const agentIds = new Set()
|
|
2240
|
-
filteredTraces.forEach((t) => { if (t.agent) agentIds.add(t.agent) })
|
|
2241
|
-
agents.forEach((a) => agentIds.add(a.agentId))
|
|
2242
|
-
const outcomes = filteredTraces.filter((t) => t.event_type === 'task_outcome')
|
|
2243
|
-
const successes = outcomes.filter((t) => t.outcome === 'success').length
|
|
2244
|
-
const failures = outcomes.filter((t) => t.outcome === 'failure').length
|
|
2245
|
-
const verifiedReuses = reuses.filter((r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved)
|
|
2246
|
-
const pendingReuses = reuses.filter((r) => r.source === 'auto_claim' && !r.confirmed)
|
|
2247
|
-
|
|
2248
|
-
const ecosystem = {
|
|
2249
|
-
agents: agentIds.size,
|
|
2250
|
-
totalTasks: outcomes.length,
|
|
2251
|
-
successRate: outcomes.length > 0 ? Math.round((successes / outcomes.length) * 100) : 0,
|
|
2252
|
-
totalMemories: filteredMemories.length,
|
|
2253
|
-
totalReuses: reuses.length,
|
|
2254
|
-
verifiedReuses: verifiedReuses.length,
|
|
2255
|
-
pendingReuses: pendingReuses.length,
|
|
2256
|
-
}
|
|
2257
|
-
|
|
2258
|
-
// Agent performance comparison
|
|
2259
|
-
const agentPerf = []
|
|
2260
|
-
for (const agentId of agentIds) {
|
|
2261
|
-
const agentTraces = filteredTraces.filter((t) => t.agent === agentId)
|
|
2262
|
-
const agentOutcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
|
|
2263
|
-
const agentSuccesses = agentOutcomes.filter((t) => t.outcome === 'success').length
|
|
2264
|
-
const agentFailures = agentOutcomes.filter((t) => t.outcome === 'failure').length
|
|
2265
|
-
const agentMemories = filteredMemories.filter((m) => m.source === agentId)
|
|
2266
|
-
const agentVerified = reuses.filter((r) =>
|
|
2267
|
-
(r.sourceAgent === agentId || r.targetAgent === agentId) && r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved
|
|
2268
|
-
)
|
|
2269
|
-
const reg = agents.find((a) => a.agentId === agentId)
|
|
2270
|
-
agentPerf.push({
|
|
2271
|
-
agentId,
|
|
2272
|
-
tasks: {
|
|
2273
|
-
total: agentOutcomes.length,
|
|
2274
|
-
success: agentSuccesses,
|
|
2275
|
-
failure: agentFailures,
|
|
2276
|
-
successRate: agentOutcomes.length > 0 ? Math.round((agentSuccesses / agentOutcomes.length) * 100) : 0,
|
|
2277
|
-
},
|
|
2278
|
-
memories: agentMemories.length,
|
|
2279
|
-
verifiedReuses: agentVerified.length,
|
|
2280
|
-
registered: !!reg,
|
|
2281
|
-
lastActive: reg ? reg.lastSeenAt : (agentTraces.length > 0 ? agentTraces[agentTraces.length - 1].timestamp : null),
|
|
2282
|
-
})
|
|
2283
|
-
}
|
|
2284
|
-
agentPerf.sort((a, b) => b.tasks.successRate - a.tasks.successRate || b.tasks.total - a.tasks.total)
|
|
2285
|
-
|
|
2286
|
-
// Cross-agent patterns
|
|
2287
|
-
const crossPatterns = []
|
|
2288
|
-
const reuseByPair = {}
|
|
2289
|
-
for (const r of reuses) {
|
|
2290
|
-
const key = r.sourceAgent + '->' + r.targetAgent
|
|
2291
|
-
if (!reuseByPair[key]) reuseByPair[key] = { source: r.sourceAgent, target: r.targetAgent, total: 0, verified: 0, improved: 0 }
|
|
2292
|
-
reuseByPair[key].total++
|
|
2293
|
-
if (r.confirmed) reuseByPair[key].verified++
|
|
2294
|
-
if (r.outcomeImproved) reuseByPair[key].improved++
|
|
2295
|
-
}
|
|
2296
|
-
for (const [key, pair] of Object.entries(reuseByPair)) {
|
|
2297
|
-
if (pair.source !== pair.target) {
|
|
2298
|
-
crossPatterns.push({
|
|
2299
|
-
type: 'reuse_path',
|
|
2300
|
-
from: pair.source,
|
|
2301
|
-
to: pair.target,
|
|
2302
|
-
total: pair.total,
|
|
2303
|
-
verified: pair.verified,
|
|
2304
|
-
improved: pair.improved,
|
|
2305
|
-
})
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
crossPatterns.sort((a, b) => b.verified - a.verified || b.total - a.total)
|
|
2309
|
-
|
|
2310
|
-
// Memory health
|
|
2311
|
-
const byKind = {}
|
|
2312
|
-
let shortContent = 0
|
|
2313
|
-
let missingKind = 0
|
|
2314
|
-
for (const m of filteredMemories) {
|
|
2315
|
-
const kind = m.kind || 'unknown'
|
|
2316
|
-
byKind[kind] = (byKind[kind] || 0) + 1
|
|
2317
|
-
const content = String(m.content || '').trim()
|
|
2318
|
-
if (content.length < 20) shortContent++
|
|
2319
|
-
if (!m.kind) missingKind++
|
|
2320
|
-
}
|
|
2321
|
-
const memoryHealth = {
|
|
2322
|
-
total: filteredMemories.length,
|
|
2323
|
-
byKind,
|
|
2324
|
-
shortContent,
|
|
2325
|
-
missingKind,
|
|
2326
|
-
qualityScore: filteredMemories.length > 0
|
|
2327
|
-
? Math.round(((filteredMemories.length - shortContent - missingKind) / filteredMemories.length) * 100)
|
|
2328
|
-
: 100,
|
|
2329
|
-
}
|
|
1112
|
+
const evolutionReportStore = createEvolutionReport({ dataDir, projectName })
|
|
2330
1113
|
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
// Routing suggestion: compare agent success rates
|
|
2335
|
-
if (agentPerf.length >= 2) {
|
|
2336
|
-
const best = agentPerf[0]
|
|
2337
|
-
const worst = agentPerf[agentPerf.length - 1]
|
|
2338
|
-
if (best.tasks.total >= 5 && worst.tasks.total >= 5 && best.tasks.successRate - worst.tasks.successRate >= 15) {
|
|
2339
|
-
suggestions.push({
|
|
2340
|
-
category: 'routing',
|
|
2341
|
-
priority: 'high',
|
|
2342
|
-
description: best.agentId + ' has ' + best.tasks.successRate + '% success rate vs ' + worst.agentId + ' at ' + worst.tasks.successRate + '%; prefer routing to ' + best.agentId,
|
|
2343
|
-
basedOn: 'task_outcome comparison',
|
|
2344
|
-
})
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
// Pending auto-claim suggestion
|
|
2349
|
-
if (pendingReuses.length >= 10) {
|
|
2350
|
-
suggestions.push({
|
|
2351
|
-
category: 'evidence',
|
|
2352
|
-
priority: 'medium',
|
|
2353
|
-
description: pendingReuses.length + ' pending auto-claims awaiting confirmation; review cross-agent claims to increase verified reuse count',
|
|
2354
|
-
basedOn: 'experience_reuse.jsonl',
|
|
2355
|
-
})
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
// Memory quality suggestion
|
|
2359
|
-
if (memoryHealth.qualityScore < 80 && filteredMemories.length > 10) {
|
|
2360
|
-
suggestions.push({
|
|
2361
|
-
category: 'memory',
|
|
2362
|
-
priority: 'medium',
|
|
2363
|
-
description: 'Memory quality score is ' + memoryHealth.qualityScore + '%; ' + shortContent + ' short records and ' + missingKind + ' missing kind fields found',
|
|
2364
|
-
basedOn: 'memory.jsonl quality analysis',
|
|
2365
|
-
})
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
// Cross-agent value suggestion
|
|
2369
|
-
if (crossPatterns.length === 0 && agentIds.size >= 2) {
|
|
2370
|
-
suggestions.push({
|
|
2371
|
-
category: 'interop',
|
|
2372
|
-
priority: 'high',
|
|
2373
|
-
description: 'Multiple agents registered but no cross-agent reuse patterns detected; ensure agents query shared memory',
|
|
2374
|
-
basedOn: 'experience_reuse.jsonl',
|
|
2375
|
-
})
|
|
2376
|
-
}
|
|
2377
|
-
|
|
2378
|
-
// Active agent suggestion
|
|
2379
|
-
const inactiveAgents = agentPerf.filter((a) => {
|
|
2380
|
-
if (!a.lastActive) return false
|
|
2381
|
-
return (now - new Date(a.lastActive).getTime()) > 7 * 86400000
|
|
2382
|
-
})
|
|
2383
|
-
if (inactiveAgents.length > 0) {
|
|
2384
|
-
suggestions.push({
|
|
2385
|
-
category: 'coverage',
|
|
2386
|
-
priority: 'low',
|
|
2387
|
-
description: inactiveAgents.map((a) => a.agentId).join(', ') + ' inactive for >7 days; consider removal or reactivation',
|
|
2388
|
-
basedOn: 'agent registry lastSeenAt',
|
|
2389
|
-
})
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
suggestions.sort((a, b) => {
|
|
2393
|
-
const p = { high: 0, medium: 1, low: 2 }
|
|
2394
|
-
return (p[a.priority] || 2) - (p[b.priority] || 2)
|
|
2395
|
-
})
|
|
2396
|
-
|
|
2397
|
-
return {
|
|
2398
|
-
project,
|
|
2399
|
-
generatedAt: new Date().toISOString(),
|
|
2400
|
-
period,
|
|
2401
|
-
ecosystem,
|
|
2402
|
-
agentPerformance: agentPerf,
|
|
2403
|
-
crossAgentPatterns: crossPatterns,
|
|
2404
|
-
memoryHealth,
|
|
2405
|
-
suggestions,
|
|
2406
|
-
}
|
|
1114
|
+
function evolutionReport(args = {}) {
|
|
1115
|
+
return evolutionReportStore.report(args)
|
|
2407
1116
|
}
|
|
2408
1117
|
|
|
2409
1118
|
|
|
2410
|
-
function recordTaskOutcome(args = {}) {
|
|
2411
|
-
const outcome = String(args.outcome || '').trim().toLowerCase()
|
|
2412
|
-
if (!['success', 'failure', 'aborted'].includes(outcome)) {
|
|
2413
|
-
throw new Error('task.record_outcome requires outcome to be success, failure, or aborted')
|
|
2414
|
-
}
|
|
2415
|
-
const agentId = String(args.agentId || runtimeAgentId() || 'unknown').trim()
|
|
2416
|
-
const project = args.project || projectName()
|
|
2417
|
-
const task = String(args.task || '').trim()
|
|
2418
|
-
const summary = String(args.summary || '').trim()
|
|
2419
|
-
const verification = String(args.verification || '').trim()
|
|
2420
|
-
const traceId = String(args.traceId || '').trim() || createId('trace:' + agentId + ':' + project)
|
|
2421
|
-
|
|
2422
|
-
// 1. Record trace event
|
|
2423
|
-
const event = {
|
|
2424
|
-
id: createId('trace'),
|
|
2425
|
-
timestamp: new Date().toISOString(),
|
|
2426
|
-
trace_id: traceId,
|
|
2427
|
-
event_type: 'task_outcome',
|
|
2428
|
-
outcome,
|
|
2429
|
-
payload: { task, summary, verification },
|
|
2430
|
-
agent: agentId,
|
|
2431
|
-
host: 'mcp',
|
|
2432
|
-
project,
|
|
2433
|
-
}
|
|
2434
|
-
appendJsonl(tracePath, event)
|
|
2435
|
-
|
|
2436
|
-
// 2. Auto-claim experience reuse
|
|
2437
|
-
const autoClaims = autoClaimExperienceReuse(event)
|
|
2438
|
-
|
|
2439
|
-
// 3. Update agent registry
|
|
2440
|
-
const now = new Date().toISOString()
|
|
2441
|
-
const agents = readJsonl(agentsPath)
|
|
2442
|
-
const agent = agents.find((a) => a.agentId === agentId && a.project === project)
|
|
2443
|
-
if (agent) {
|
|
2444
|
-
agent.lastSeenAt = now
|
|
2445
|
-
agent.taskCount = (agent.taskCount || 0) + 1
|
|
2446
|
-
agent.successCount = (agent.successCount || 0) + (outcome === 'success' ? 1 : 0)
|
|
2447
|
-
agent.failureCount = (agent.failureCount || 0) + (outcome === 'failure' ? 1 : 0)
|
|
2448
|
-
writeJsonl(agentsPath, agents)
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
return {
|
|
2452
|
-
recorded: true,
|
|
2453
|
-
event: { id: event.id, trace_id: traceId, outcome, agent: agentId, project },
|
|
2454
|
-
agentUpdated: !!agent,
|
|
2455
|
-
autoClaims: autoClaims.length > 0 ? autoClaims : undefined,
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
1119
|
|
|
2459
1120
|
module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl, rl }
|