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
package/observe/observer.js
CHANGED
|
@@ -65,6 +65,71 @@ function stateFile(home) {
|
|
|
65
65
|
return path.join(home, 'observe-state.json')
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// Canonical form for a workspace path used as a context key.
|
|
69
|
+
//
|
|
70
|
+
// Context keys are taken verbatim from the `cwd` field of host transcripts, and
|
|
71
|
+
// hosts disagree on spelling: the same directory arrives as `D:\work\code\x`,
|
|
72
|
+
// `d:\work\code\x`, or `D:/work/code/x`. Left alone, one project splits into
|
|
73
|
+
// several buckets, each capped at CONTEXT_MAX, so its history is fragmented and
|
|
74
|
+
// entries are dropped early. Windows paths are case-insensitive, so normalise
|
|
75
|
+
// the drive letter to upper case and use backslashes consistently to match the
|
|
76
|
+
// style the file-observation keys already use.
|
|
77
|
+
function normalizeCwd(cwd) {
|
|
78
|
+
if (!cwd || typeof cwd !== 'string') return cwd
|
|
79
|
+
let out = cwd.replace(/\//g, '\\').replace(/\\+/g, '\\')
|
|
80
|
+
// Upper-case a single drive letter prefix ("c:\..." -> "C:\...").
|
|
81
|
+
if (/^[a-zA-Z]:\\/.test(out)) out = out[0].toUpperCase() + out.slice(1)
|
|
82
|
+
// Drop a trailing separator unless the path is just the drive root.
|
|
83
|
+
if (out.length > 3 && out.endsWith('\\')) out = out.slice(0, -1)
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse the "YYYY/M/D HH:MM:SS | ..." stamp that context lines carry.
|
|
88
|
+
// Returns null when the line has no recognisable leading timestamp, so
|
|
89
|
+
// unparseable entries never get treated as the oldest by accident.
|
|
90
|
+
function contextTimestamp(line) {
|
|
91
|
+
const m = /^\s*(\d{4})\/(\d{1,2})\/(\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})/.exec(String(line || ''))
|
|
92
|
+
if (!m) return null
|
|
93
|
+
const ms = new Date(
|
|
94
|
+
Number(m[1]),
|
|
95
|
+
Number(m[2]) - 1,
|
|
96
|
+
Number(m[3]),
|
|
97
|
+
Number(m[4]),
|
|
98
|
+
Number(m[5]),
|
|
99
|
+
Number(m[6]),
|
|
100
|
+
).getTime()
|
|
101
|
+
return Number.isFinite(ms) ? ms : null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Drop repeated lines from stored context lists, preserving the most recent
|
|
105
|
+
// occurrence order. Older builds could write the same line into every slot of
|
|
106
|
+
// a workspace block (a generated digest headline broadcast to all projects),
|
|
107
|
+
// so existing observe-state files still carry duplicates until they are
|
|
108
|
+
// rewritten. Normalising on load heals that data without a migration step.
|
|
109
|
+
//
|
|
110
|
+
// Keys are also folded through normalizeCwd and merged, so buckets split by
|
|
111
|
+
// spelling differences collapse back into one on the next load.
|
|
112
|
+
function dedupeContexts(contexts) {
|
|
113
|
+
const out = {}
|
|
114
|
+
for (const [cwd, list] of Object.entries(contexts)) {
|
|
115
|
+
const key = normalizeCwd(cwd)
|
|
116
|
+
const existing = out[key] || []
|
|
117
|
+
if (!Array.isArray(list)) {
|
|
118
|
+
out[key] = existing
|
|
119
|
+
continue
|
|
120
|
+
}
|
|
121
|
+
const seen = new Set(existing)
|
|
122
|
+
const merged = existing.slice()
|
|
123
|
+
for (const line of list) {
|
|
124
|
+
if (seen.has(line)) continue
|
|
125
|
+
seen.add(line)
|
|
126
|
+
merged.push(line)
|
|
127
|
+
}
|
|
128
|
+
out[key] = merged
|
|
129
|
+
}
|
|
130
|
+
return out
|
|
131
|
+
}
|
|
132
|
+
|
|
68
133
|
function loadState(home) {
|
|
69
134
|
try {
|
|
70
135
|
const parsed = JSON.parse(fs.readFileSync(stateFile(home), 'utf8'))
|
|
@@ -74,7 +139,7 @@ function loadState(home) {
|
|
|
74
139
|
files: parsed.files || {},
|
|
75
140
|
buffers: parsed.buffers || {},
|
|
76
141
|
openTurns: parsed.openTurns || {},
|
|
77
|
-
contexts: parsed.contexts || {},
|
|
142
|
+
contexts: dedupeContexts(parsed.contexts || {}),
|
|
78
143
|
codexFiles: parsed.codexFiles || {},
|
|
79
144
|
codexBuffers: parsed.codexBuffers || {},
|
|
80
145
|
codexTurns: parsed.codexTurns || {},
|
|
@@ -290,11 +355,39 @@ const CONTEXT_MAX = 6
|
|
|
290
355
|
// retrieval guaranteed (not dependent on the model deciding to read memory).
|
|
291
356
|
function updateProjectContext(state, cwd, line, targetFile) {
|
|
292
357
|
if (!cwd || !line) return
|
|
358
|
+
const key = normalizeCwd(cwd)
|
|
293
359
|
const target = path.join(cwd, targetFile || 'AGENTS.md')
|
|
294
|
-
|
|
360
|
+
// Merge any buckets that differ only by spelling, so a project observed as
|
|
361
|
+
// both "D:\work\code\x" and "d:\work\code\x" converges into one list instead
|
|
362
|
+
// of two half-filled ones.
|
|
363
|
+
const list = []
|
|
364
|
+
const seen = new Set()
|
|
365
|
+
for (const [existingKey, items] of Object.entries(state.contexts || {})) {
|
|
366
|
+
if (normalizeCwd(existingKey) !== key) continue
|
|
367
|
+
for (const item of items || []) {
|
|
368
|
+
if (seen.has(item)) continue
|
|
369
|
+
seen.add(item)
|
|
370
|
+
list.push(item)
|
|
371
|
+
}
|
|
372
|
+
if (existingKey !== key) delete state.contexts[existingKey]
|
|
373
|
+
}
|
|
374
|
+
// Re-inserting an existing line moves it to the front instead of duplicating.
|
|
375
|
+
const existing = list.indexOf(line)
|
|
376
|
+
if (existing !== -1) list.splice(existing, 1)
|
|
295
377
|
list.unshift(line)
|
|
296
|
-
|
|
297
|
-
|
|
378
|
+
// Once buckets merge, the list can exceed CONTEXT_MAX. Entries are stamped
|
|
379
|
+
// "YYYY/M/D HH:MM:SS | ...", so trim by that timestamp rather than by
|
|
380
|
+
// position -- merging appends whole buckets in object-key order, which says
|
|
381
|
+
// nothing about age, and popping blind could discard recent work.
|
|
382
|
+
if (list.length > CONTEXT_MAX) {
|
|
383
|
+
const stamped = list.filter((l) => contextTimestamp(l) !== null)
|
|
384
|
+
if (stamped.length > 1) {
|
|
385
|
+
list.sort((a, b) => contextTimestamp(b) - contextTimestamp(a))
|
|
386
|
+
}
|
|
387
|
+
list.length = CONTEXT_MAX
|
|
388
|
+
}
|
|
389
|
+
state.contexts = state.contexts || {}
|
|
390
|
+
state.contexts[key] = list
|
|
298
391
|
try {
|
|
299
392
|
let content = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
300
393
|
const block = CONTEXT_BEGIN + '\n## Mio 最近上下文(自动注入)\n' + list.map(function (l) { return '- ' + l }).join('\n') + '\n' + CONTEXT_END
|
|
@@ -1591,6 +1684,9 @@ module.exports = {
|
|
|
1591
1684
|
workbuddyConfigDir,
|
|
1592
1685
|
loadState,
|
|
1593
1686
|
saveState,
|
|
1687
|
+
dedupeContexts,
|
|
1688
|
+
normalizeCwd,
|
|
1689
|
+
contextTimestamp,
|
|
1594
1690
|
updateProjectContext,
|
|
1595
1691
|
runCycle,
|
|
1596
1692
|
opencodeDbPath,
|
package/package.json
CHANGED
|
@@ -1,38 +1,45 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "commonjs",
|
|
7
|
-
"bin": {
|
|
8
|
-
"mio": "bin/mio.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"bin/",
|
|
12
|
-
"server/",
|
|
13
|
-
"adapters/",
|
|
14
|
-
"observe/",
|
|
15
|
-
"scripts/",
|
|
16
|
-
"README.md",
|
|
17
|
-
"!server/mio-intelligence-mcp/__tests__"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"check": "node
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"@akemi-mio/runtime-
|
|
30
|
-
"@akemi-mio/
|
|
31
|
-
"@akemi-mio/
|
|
32
|
-
"@akemi-mio/evolution-
|
|
33
|
-
"@akemi-mio/evolution-
|
|
34
|
-
"@akemi-mio/evolution-
|
|
35
|
-
"@akemi-mio/
|
|
36
|
-
"@akemi-mio/
|
|
37
|
-
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mio-agent-runtime",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mio": "bin/mio.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"server/",
|
|
13
|
+
"adapters/",
|
|
14
|
+
"observe/",
|
|
15
|
+
"scripts/",
|
|
16
|
+
"README.md",
|
|
17
|
+
"!server/mio-intelligence-mcp/__tests__"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"check": "node scripts/check-syntax.cjs",
|
|
21
|
+
"test": "node --test \"__tests__/*.test.js\" \"server/mio-intelligence-mcp/__tests__/*.test.js\"",
|
|
22
|
+
"prepack": "npm run check",
|
|
23
|
+
"verify:pack-install": "node scripts/verify-packed-runtime.js"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@akemi-mio/runtime-contracts": "^0.1.0",
|
|
30
|
+
"@akemi-mio/runtime-foundation": "^0.1.0",
|
|
31
|
+
"@akemi-mio/experience-memory": "^0.1.0",
|
|
32
|
+
"@akemi-mio/evolution-learning": "^0.1.0",
|
|
33
|
+
"@akemi-mio/evolution-strategy": "^0.1.0",
|
|
34
|
+
"@akemi-mio/evolution-safety": "^0.1.0",
|
|
35
|
+
"@akemi-mio/evolution-scheduler": "^0.1.0",
|
|
36
|
+
"@akemi-mio/observer": "^0.1.0",
|
|
37
|
+
"@akemi-mio/insight": "^0.1.0"
|
|
38
|
+
},
|
|
39
|
+
"main": "index.js",
|
|
40
|
+
"directories": {
|
|
41
|
+
"doc": "docs"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [],
|
|
44
|
+
"author": ""
|
|
45
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Syntax gate for mio-agent-runtime (`npm run check`, which prepack runs).
|
|
5
|
+
//
|
|
6
|
+
// This used to be a hand-written file list in package.json. That silently rotted:
|
|
7
|
+
// nine server modules -- including every store added later (policy, agent, task,
|
|
8
|
+
// query-log, llm-client, creativity-engine) -- were never syntax-checked, so a
|
|
9
|
+
// typo in any of them would have survived `npm run check` and only exploded at
|
|
10
|
+
// require time, i.e. inside the published package.
|
|
11
|
+
//
|
|
12
|
+
// So: discover files instead of listing them. New modules are covered
|
|
13
|
+
// automatically, and the gate can no longer fall behind the codebase.
|
|
14
|
+
|
|
15
|
+
const fs = require('fs')
|
|
16
|
+
const path = require('path')
|
|
17
|
+
const { spawnSync } = require('child_process')
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(__dirname, '..')
|
|
20
|
+
|
|
21
|
+
// Top-level dirs to walk. __tests__ is excluded: those are exercised by
|
|
22
|
+
// `node --test`, and a broken test file should fail the test run, not the
|
|
23
|
+
// package gate.
|
|
24
|
+
const INCLUDE_DIRS = ['bin', 'adapters', 'observe', 'server', 'scripts']
|
|
25
|
+
const EXCLUDE_DIRS = new Set(['node_modules', '__tests__', 'dist', 'out'])
|
|
26
|
+
const EXTENSIONS = new Set(['.js', '.cjs'])
|
|
27
|
+
|
|
28
|
+
function walk(dir, out = []) {
|
|
29
|
+
let entries
|
|
30
|
+
try {
|
|
31
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
32
|
+
} catch (_) {
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
if (EXCLUDE_DIRS.has(entry.name)) continue
|
|
38
|
+
walk(path.join(dir, entry.name), out)
|
|
39
|
+
} else if (entry.isFile() && EXTENSIONS.has(path.extname(entry.name))) {
|
|
40
|
+
out.push(path.join(dir, entry.name))
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const files = INCLUDE_DIRS.flatMap((dir) => walk(path.join(ROOT, dir))).sort()
|
|
47
|
+
|
|
48
|
+
if (files.length === 0) {
|
|
49
|
+
console.error('check-syntax: no files discovered -- the gate is broken')
|
|
50
|
+
process.exit(1)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const failures = []
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const result = spawnSync(process.execPath, ['--check', file], { encoding: 'utf8' })
|
|
56
|
+
if (result.status !== 0) {
|
|
57
|
+
failures.push({ file, message: (result.stderr || '').trim().split('\n')[0] })
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const rel = (f) => path.relative(ROOT, f).replace(/\\/g, '/')
|
|
62
|
+
console.log(`checked ${files.length} file(s)`)
|
|
63
|
+
|
|
64
|
+
if (failures.length > 0) {
|
|
65
|
+
for (const failure of failures) {
|
|
66
|
+
console.error(`FAIL ${rel(failure.file)}: ${failure.message}`)
|
|
67
|
+
}
|
|
68
|
+
console.error(`\n${failures.length} file(s) failed syntax check`)
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('all files parse')
|
|
@@ -76,12 +76,13 @@ function npmCommand(platform = process.platform) {
|
|
|
76
76
|
|
|
77
77
|
function npmInvocation(args, env = process.env, platform = process.platform, node = process.execPath) {
|
|
78
78
|
if (env.npm_execpath) {
|
|
79
|
-
return { command: node, args: [env.npm_execpath, ...args] }
|
|
79
|
+
return { command: node, args: [env.npm_execpath, ...args], shell: false }
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
// Fall back to the npm shim on PATH instead of routing through cmd.exe.
|
|
82
|
+
// cmd.exe is unavailable in some sandboxes, and a bare .cmd/.bat shim cannot
|
|
83
|
+
// be spawned directly (Node requires shell:true for those on Windows), so
|
|
84
|
+
// signal the shell requirement to the runner instead of guessing there.
|
|
85
|
+
return { command: npmCommand(platform), args: [...args], shell: platform === 'win32' }
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
function packageDirName(name) {
|
|
@@ -95,9 +96,12 @@ function tarballFileName(name, version) {
|
|
|
95
96
|
|
|
96
97
|
function readPackageJson(workspaceRoot, name) {
|
|
97
98
|
const packageDir = path.join(workspaceRoot, 'packages', packageDirName(name))
|
|
99
|
+
// Windows editors may write package.json with a UTF-8 BOM; JSON.parse
|
|
100
|
+
// rejects the leading U+FEFF, so strip it before parsing.
|
|
101
|
+
const raw = fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8').replace(/^\uFEFF/, '')
|
|
98
102
|
return {
|
|
99
103
|
packageDir,
|
|
100
|
-
manifest: JSON.parse(
|
|
104
|
+
manifest: JSON.parse(raw),
|
|
101
105
|
}
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -211,6 +215,11 @@ function run(command, args, options = {}) {
|
|
|
211
215
|
env: options.env || process.env,
|
|
212
216
|
encoding: 'utf8',
|
|
213
217
|
stdio: options.stdio || ['ignore', 'pipe', 'pipe'],
|
|
218
|
+
// Windows .cmd/.bat shims (npm.cmd) cannot be spawned directly by
|
|
219
|
+
// execFileSync since Node's CVE-2024-27980 hardening; they need a shell.
|
|
220
|
+
// This only kicks in for the npm shim path — node-based invocations pass
|
|
221
|
+
// an absolute node executable and still run shell-free.
|
|
222
|
+
shell: options.shell === true,
|
|
214
223
|
})
|
|
215
224
|
return output ? output.trim() : ''
|
|
216
225
|
}
|
|
@@ -296,14 +305,19 @@ function verifyPackedRuntimeInstall(input = {}) {
|
|
|
296
305
|
run(pack.command, pack.args, {
|
|
297
306
|
cwd: pkg.packageDir,
|
|
298
307
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
308
|
+
shell: pack.shell,
|
|
299
309
|
})
|
|
300
310
|
packageArtifactEvidence(pkg)
|
|
301
311
|
}
|
|
302
312
|
|
|
303
313
|
const init = npmInvocation(['init', '-y'])
|
|
304
|
-
run(init.command, init.args, { cwd: plan.installDir, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
314
|
+
run(init.command, init.args, { cwd: plan.installDir, stdio: ['ignore', 'pipe', 'pipe'], shell: init.shell })
|
|
305
315
|
const install = npmInvocation(plan.install.args)
|
|
306
|
-
run(install.command, install.args, {
|
|
316
|
+
run(install.command, install.args, {
|
|
317
|
+
cwd: plan.install.cwd,
|
|
318
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
319
|
+
shell: install.shell,
|
|
320
|
+
})
|
|
307
321
|
const smokeEnv = {
|
|
308
322
|
...process.env,
|
|
309
323
|
MIO_HOME: path.join(plan.installDir, '.mio-home'),
|
|
@@ -360,6 +374,7 @@ module.exports = {
|
|
|
360
374
|
createVerifyPackedRuntimeOptions,
|
|
361
375
|
createPackedRuntimeInstallPlan,
|
|
362
376
|
packageArtifactEvidence,
|
|
377
|
+
readPackageJson,
|
|
363
378
|
npmInvocation,
|
|
364
379
|
npmCommand,
|
|
365
380
|
parseVerifyPackedRuntimeArgs,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Shared agent store: the single implementation behind mio.agent.list /
|
|
4
|
+
// register / report, consumed by both the MCP server and the CLI
|
|
5
|
+
// (`mio agents list` / `mio agents report`). It owns agents.jsonl plus the
|
|
6
|
+
// read-only cross-references the report needs (traces, memory, experience
|
|
7
|
+
// reuse). Extracted from mio-intelligence-mcp/index.js so the terminal and the
|
|
8
|
+
// MCP tool can never report different agent telemetry.
|
|
9
|
+
//
|
|
10
|
+
// Two distinct "agent" notions live in this repo and the CLI must not confuse
|
|
11
|
+
// them: `mio agents` (no subcommand) lists installed host adapters from
|
|
12
|
+
// config.json, while this store reports *observed* agents recorded in
|
|
13
|
+
// agents.jsonl. `mio agents list` / `report` are about the latter.
|
|
14
|
+
|
|
15
|
+
const path = require('path')
|
|
16
|
+
const { createId, readJsonl, appendJsonl, writeJsonl } = require('./memory-store.js')
|
|
17
|
+
|
|
18
|
+
function createAgentStore(options) {
|
|
19
|
+
const dataDir = options.dataDir
|
|
20
|
+
const projectName = options.projectName || (() => null)
|
|
21
|
+
|
|
22
|
+
const agentsPath = path.join(dataDir, 'agents.jsonl')
|
|
23
|
+
const tracePath = path.join(dataDir, 'traces.jsonl')
|
|
24
|
+
const memoryPath = path.join(dataDir, 'memory.jsonl')
|
|
25
|
+
const experienceReusePath = path.join(dataDir, 'experience_reuse.jsonl')
|
|
26
|
+
|
|
27
|
+
function listAgents(args = {}) {
|
|
28
|
+
const project = args.project || null
|
|
29
|
+
let agents = readJsonl(agentsPath)
|
|
30
|
+
if (project) agents = agents.filter((a) => a.project === project)
|
|
31
|
+
return {
|
|
32
|
+
count: agents.length,
|
|
33
|
+
agents: agents.map((a) => ({
|
|
34
|
+
id: a.id,
|
|
35
|
+
agentId: a.agentId,
|
|
36
|
+
hostType: a.hostType,
|
|
37
|
+
capabilities: a.capabilities,
|
|
38
|
+
registeredAt: a.registeredAt,
|
|
39
|
+
lastSeenAt: a.lastSeenAt,
|
|
40
|
+
sessionCount: a.sessionCount,
|
|
41
|
+
taskCount: a.taskCount || 0,
|
|
42
|
+
successCount: a.successCount || 0,
|
|
43
|
+
failureCount: a.failureCount || 0,
|
|
44
|
+
project: a.project,
|
|
45
|
+
})),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function reportAgent(args = {}) {
|
|
50
|
+
const targetAgent = args.agentId || null
|
|
51
|
+
const project = args.project || projectName()
|
|
52
|
+
const agents = readJsonl(agentsPath)
|
|
53
|
+
const filtered = agents.filter(
|
|
54
|
+
(a) => (!targetAgent || a.agentId === targetAgent) && (!project || a.project === project)
|
|
55
|
+
)
|
|
56
|
+
const traces = readJsonl(tracePath)
|
|
57
|
+
const memories = readJsonl(memoryPath)
|
|
58
|
+
const reuses = readJsonl(experienceReusePath)
|
|
59
|
+
const reports = filtered.map((agent) => {
|
|
60
|
+
const agentTraces = traces.filter((t) => t.agent === agent.agentId)
|
|
61
|
+
const outcomes = agentTraces.filter((t) => t.event_type === 'task_outcome')
|
|
62
|
+
const successes = outcomes.filter((t) => t.outcome === 'success').length
|
|
63
|
+
const failures = outcomes.filter((t) => t.outcome === 'failure').length
|
|
64
|
+
const agentMemories = memories.filter((m) => m.source === agent.agentId)
|
|
65
|
+
const agentReuses = reuses.filter(
|
|
66
|
+
(r) => r.sourceAgent === agent.agentId || r.targetAgent === agent.agentId
|
|
67
|
+
)
|
|
68
|
+
const verified = agentReuses.filter(
|
|
69
|
+
(r) => r.confirmed && r.reuse && r.behaviorChanged && r.outcomeImproved
|
|
70
|
+
)
|
|
71
|
+
const regTasks = agent.taskCount || 0
|
|
72
|
+
const regSuccess = agent.successCount || 0
|
|
73
|
+
const regFail = agent.failureCount || 0
|
|
74
|
+
const taskTotal = regTasks > 0 ? regTasks : outcomes.length
|
|
75
|
+
const taskSuccess = regTasks > 0 ? regSuccess : successes
|
|
76
|
+
const taskFail = regTasks > 0 ? regFail : failures
|
|
77
|
+
return {
|
|
78
|
+
agentId: agent.agentId,
|
|
79
|
+
hostType: agent.hostType,
|
|
80
|
+
sessionCount: agent.sessionCount,
|
|
81
|
+
registeredAt: agent.registeredAt,
|
|
82
|
+
lastSeenAt: agent.lastSeenAt,
|
|
83
|
+
taskOutcomes: {
|
|
84
|
+
total: taskTotal,
|
|
85
|
+
success: taskSuccess,
|
|
86
|
+
failure: taskFail,
|
|
87
|
+
successRate: taskTotal > 0 ? Math.round((taskSuccess / taskTotal) * 100) : 0,
|
|
88
|
+
},
|
|
89
|
+
memories: agentMemories.length,
|
|
90
|
+
experienceReuses: { total: agentReuses.length, verified: verified.length },
|
|
91
|
+
active: Date.now() - new Date(agent.lastSeenAt).getTime() < 24 * 60 * 60 * 1000,
|
|
92
|
+
}
|
|
93
|
+
})
|
|
94
|
+
return { project, count: reports.length, reports }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function registerAgent(args = {}) {
|
|
98
|
+
const agentId = String(args.agentId || '').trim()
|
|
99
|
+
if (!agentId) throw new Error('agent.register requires a non-empty agentId')
|
|
100
|
+
const project = args.project || projectName()
|
|
101
|
+
const hostType = args.hostType || 'mcp'
|
|
102
|
+
const capabilities = Array.isArray(args.capabilities) ? args.capabilities : []
|
|
103
|
+
const now = new Date().toISOString()
|
|
104
|
+
const agents = readJsonl(agentsPath)
|
|
105
|
+
const existing = agents.find((a) => a.agentId === agentId && a.project === project)
|
|
106
|
+
let result
|
|
107
|
+
if (existing) {
|
|
108
|
+
existing.lastSeenAt = now
|
|
109
|
+
existing.sessionCount = (existing.sessionCount || 1) + 1
|
|
110
|
+
existing.hostType = hostType
|
|
111
|
+
if (capabilities.length > 0) existing.capabilities = capabilities
|
|
112
|
+
result = existing
|
|
113
|
+
} else {
|
|
114
|
+
result = {
|
|
115
|
+
id: createId('agent'),
|
|
116
|
+
agentId,
|
|
117
|
+
hostType,
|
|
118
|
+
capabilities,
|
|
119
|
+
registeredAt: now,
|
|
120
|
+
lastSeenAt: now,
|
|
121
|
+
sessionCount: 1,
|
|
122
|
+
project,
|
|
123
|
+
}
|
|
124
|
+
agents.push(result)
|
|
125
|
+
}
|
|
126
|
+
writeJsonl(agentsPath, agents)
|
|
127
|
+
return {
|
|
128
|
+
id: result.id,
|
|
129
|
+
agentId: result.agentId,
|
|
130
|
+
registered: !existing,
|
|
131
|
+
sessionCount: result.sessionCount,
|
|
132
|
+
hostType: result.hostType,
|
|
133
|
+
capabilities: result.capabilities,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { listAgents, reportAgent, registerAgent }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = { createAgentStore }
|
package/server/digest.js
CHANGED
|
@@ -186,6 +186,18 @@ function createDigest(options = {}) {
|
|
|
186
186
|
suggestions,
|
|
187
187
|
}
|
|
188
188
|
report.markdown = renderMarkdown(report)
|
|
189
|
+
|
|
190
|
+
// Persist a machine-readable snapshot for consumers that should not
|
|
191
|
+
// recompute the aggregation — notably mio.task.route's agent-health signal.
|
|
192
|
+
try {
|
|
193
|
+
const digestDir = path.join(home, 'digest')
|
|
194
|
+
fs.mkdirSync(digestDir, { recursive: true })
|
|
195
|
+
const snapshot = { ...report }
|
|
196
|
+
delete snapshot.markdown
|
|
197
|
+
fs.writeFileSync(path.join(digestDir, 'latest.json'), JSON.stringify(snapshot) + '\n', 'utf8')
|
|
198
|
+
} catch (_) {
|
|
199
|
+
// best-effort persistence; the in-memory report is still returned
|
|
200
|
+
}
|
|
189
201
|
return report
|
|
190
202
|
}
|
|
191
203
|
|