mio-agent-runtime 0.1.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 +41 -0
- package/adapters/codex.js +62 -0
- package/adapters/opencode.js +14 -0
- package/adapters/workbuddy.js +14 -0
- package/bin/mio.js +180 -0
- package/package.json +23 -0
- package/server/mio-intelligence-mcp/index.js +432 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# mio-agent-runtime
|
|
2
|
+
|
|
3
|
+
Mio Agent Runtime 的可发布 CLI 包。
|
|
4
|
+
|
|
5
|
+
安装一次,所有 Agent 自动接入 Mio。
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g mio-agent-runtime
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
mio init Initialize MIO_HOME
|
|
17
|
+
mio mcp Start Mio MCP server (stdio)
|
|
18
|
+
mio install <host> Install Mio into a host (codex|opencode|workbuddy)
|
|
19
|
+
mio status Show runtime and adapter status
|
|
20
|
+
mio agents List installed agents
|
|
21
|
+
mio --json status Machine-readable status
|
|
22
|
+
mio --json agents Machine-readable agents
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## MIO_HOME
|
|
26
|
+
|
|
27
|
+
默认 `~/.mio-intelligence`,可通过环境变量 `MIO_HOME` 覆盖。
|
|
28
|
+
|
|
29
|
+
## Package layout
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
packages/mio-cli
|
|
33
|
+
├── bin/mio.js
|
|
34
|
+
├── adapters/
|
|
35
|
+
│ ├── codex.js
|
|
36
|
+
│ ├── opencode.js
|
|
37
|
+
│ └── workbuddy.js
|
|
38
|
+
├── server/
|
|
39
|
+
│ └── mio-intelligence-mcp/index.js
|
|
40
|
+
└── package.json
|
|
41
|
+
```
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
|
|
7
|
+
function configPath() {
|
|
8
|
+
return path.join(os.homedir(), '.codex', 'config.toml')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function isInstalled() {
|
|
12
|
+
const target = configPath()
|
|
13
|
+
if (!fs.existsSync(target)) return false
|
|
14
|
+
return fs.readFileSync(target, 'utf8').includes('[mcp_servers.mio-intelligence]')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function install({ node, serverScript, home, workspace }) {
|
|
18
|
+
const target = configPath()
|
|
19
|
+
const backup = `${target}.bak-mio-${Date.now()}`
|
|
20
|
+
if (isInstalled()) {
|
|
21
|
+
return {
|
|
22
|
+
changed: false,
|
|
23
|
+
configPath: target,
|
|
24
|
+
message: 'Codex Mio MCP already installed.',
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(target)) {
|
|
29
|
+
fs.copyFileSync(target, backup)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const context = JSON.stringify({
|
|
33
|
+
agentId: 'codex',
|
|
34
|
+
workspace: workspace || process.cwd(),
|
|
35
|
+
sessionId: 'codex-session',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
39
|
+
let toml = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
40
|
+
if (!toml.endsWith('\n')) toml += '\n'
|
|
41
|
+
const section = [
|
|
42
|
+
'[mcp_servers.mio-intelligence]',
|
|
43
|
+
`command = '${node}'`,
|
|
44
|
+
`args = ['${serverScript}']`,
|
|
45
|
+
'startup_timeout_sec = 30',
|
|
46
|
+
'',
|
|
47
|
+
'[mcp_servers.mio-intelligence.env]',
|
|
48
|
+
`MIO_DATA_DIR = '${home}'`,
|
|
49
|
+
`MIO_CONTEXT = '${context}'`,
|
|
50
|
+
'',
|
|
51
|
+
].join('\n')
|
|
52
|
+
fs.writeFileSync(target, `${toml}${section}\n`, 'utf8')
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
changed: true,
|
|
56
|
+
configPath: target,
|
|
57
|
+
backup,
|
|
58
|
+
message: 'Codex Mio MCP installed. Restart Codex to load it.',
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { configPath, isInstalled, install }
|
package/bin/mio.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
|
|
8
|
+
const SERVER_SCRIPT = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'index.js')
|
|
9
|
+
const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
|
|
10
|
+
const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
|
|
11
|
+
|
|
12
|
+
const adapters = {
|
|
13
|
+
codex: require('../adapters/codex.js'),
|
|
14
|
+
opencode: require('../adapters/opencode.js'),
|
|
15
|
+
workbuddy: require('../adapters/workbuddy.js'),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureHome() {
|
|
19
|
+
fs.mkdirSync(MIO_HOME, { recursive: true })
|
|
20
|
+
fs.mkdirSync(path.join(MIO_HOME, 'logs'), { recursive: true })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readConfig() {
|
|
24
|
+
const fallback = { version: 1, home: MIO_HOME, agents: {} }
|
|
25
|
+
if (!fs.existsSync(CONFIG_FILE)) return fallback
|
|
26
|
+
try {
|
|
27
|
+
return { ...fallback, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
|
|
28
|
+
} catch (_) {
|
|
29
|
+
return fallback
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function writeConfig(config) {
|
|
34
|
+
ensureHome()
|
|
35
|
+
fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function jsonOrText(payload, useJson) {
|
|
39
|
+
if (useJson) {
|
|
40
|
+
console.log(JSON.stringify(payload, null, 2))
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
return payload
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function initCommand() {
|
|
47
|
+
ensureHome()
|
|
48
|
+
const config = readConfig()
|
|
49
|
+
if (!config.createdAt) config.createdAt = new Date().toISOString()
|
|
50
|
+
writeConfig(config)
|
|
51
|
+
console.log(`MIO_HOME ready: ${MIO_HOME}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function statusCommand(useJson) {
|
|
55
|
+
const config = readConfig()
|
|
56
|
+
const codex = adapters.codex
|
|
57
|
+
const installed = codex.isInstalled()
|
|
58
|
+
const payload = {
|
|
59
|
+
version: config.version,
|
|
60
|
+
home: MIO_HOME,
|
|
61
|
+
serverScript: SERVER_SCRIPT,
|
|
62
|
+
serverScriptExists: fs.existsSync(SERVER_SCRIPT),
|
|
63
|
+
agents: config.agents || {},
|
|
64
|
+
codexInstalled: installed,
|
|
65
|
+
}
|
|
66
|
+
if (useJson) {
|
|
67
|
+
jsonOrText(payload, true)
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
console.log(`MIO_HOME: ${MIO_HOME}`)
|
|
71
|
+
console.log(`MCP Server: ${SERVER_SCRIPT}`)
|
|
72
|
+
console.log(`Codex MCP: ${installed ? 'installed' : 'not installed'}`)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function agentsCommand(useJson) {
|
|
76
|
+
const config = readConfig()
|
|
77
|
+
const agents = config.agents || {}
|
|
78
|
+
if (useJson) {
|
|
79
|
+
jsonOrText(agents, true)
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
const names = Object.keys(agents)
|
|
83
|
+
if (names.length === 0) {
|
|
84
|
+
console.log('No agents installed.')
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
for (const name of names) {
|
|
88
|
+
console.log(`${name}\t${agents[name].installedAt || ''}`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function installCommand(host, useJson) {
|
|
93
|
+
if (!host) {
|
|
94
|
+
console.error('Usage: mio install <codex|opencode|workbuddy>')
|
|
95
|
+
process.exitCode = 1
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
const adapter = adapters[host]
|
|
99
|
+
if (!adapter) {
|
|
100
|
+
console.error(`Unknown host: ${host}`)
|
|
101
|
+
process.exitCode = 1
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
ensureHome()
|
|
105
|
+
const config = readConfig()
|
|
106
|
+
let result
|
|
107
|
+
try {
|
|
108
|
+
result = adapter.install({
|
|
109
|
+
node: process.execPath,
|
|
110
|
+
serverScript: SERVER_SCRIPT,
|
|
111
|
+
home: MIO_HOME,
|
|
112
|
+
workspace: process.cwd(),
|
|
113
|
+
})
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error(`Failed to install ${host}: ${error.message || error}`)
|
|
116
|
+
process.exitCode = 1
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
if (result.changed) {
|
|
120
|
+
config.agents = config.agents || {}
|
|
121
|
+
config.agents[host] = {
|
|
122
|
+
installedAt: new Date().toISOString(),
|
|
123
|
+
configPath: result.configPath || null,
|
|
124
|
+
}
|
|
125
|
+
writeConfig(config)
|
|
126
|
+
}
|
|
127
|
+
if (useJson) {
|
|
128
|
+
jsonOrText(result, true)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
console.log(result.message || `${host} adapter completed.`)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function mcpCommand() {
|
|
135
|
+
require(SERVER_SCRIPT)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function help() {
|
|
139
|
+
console.log(`Mio Agent Runtime CLI
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
mio init Initialize MIO_HOME
|
|
143
|
+
mio mcp Start Mio MCP server (stdio)
|
|
144
|
+
mio install <host> Install Mio into a host (codex|opencode|workbuddy)
|
|
145
|
+
mio status Show runtime and adapter status
|
|
146
|
+
mio agents List installed agents
|
|
147
|
+
mio --json status Machine-readable status
|
|
148
|
+
mio --json agents Machine-readable agents
|
|
149
|
+
`)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function main() {
|
|
153
|
+
const args = process.argv.slice(2)
|
|
154
|
+
const jsonIndex = args.indexOf('--json')
|
|
155
|
+
const useJson = jsonIndex !== -1
|
|
156
|
+
if (useJson) args.splice(jsonIndex, 1)
|
|
157
|
+
const command = args[0] || 'help'
|
|
158
|
+
|
|
159
|
+
switch (command) {
|
|
160
|
+
case 'init':
|
|
161
|
+
return initCommand()
|
|
162
|
+
case 'mcp':
|
|
163
|
+
return mcpCommand()
|
|
164
|
+
case 'install':
|
|
165
|
+
return installCommand(args[1], useJson)
|
|
166
|
+
case 'status':
|
|
167
|
+
return statusCommand(useJson)
|
|
168
|
+
case 'agents':
|
|
169
|
+
return agentsCommand(useJson)
|
|
170
|
+
case 'help':
|
|
171
|
+
case '--help':
|
|
172
|
+
return help()
|
|
173
|
+
default:
|
|
174
|
+
console.error(`Unknown command: ${command}`)
|
|
175
|
+
help()
|
|
176
|
+
process.exitCode = 1
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mio-agent-runtime",
|
|
3
|
+
"version": "0.1.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
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"check": "node --check bin/mio.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check server/mio-intelligence-mcp/index.js",
|
|
18
|
+
"prepack": "npm run check"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const crypto = require('crypto')
|
|
7
|
+
const readline = require('readline')
|
|
8
|
+
const { execFileSync } = require('child_process')
|
|
9
|
+
|
|
10
|
+
const SERVER_INFO = { name: 'mio-intelligence-mcp', version: '0.1.0' }
|
|
11
|
+
const PROTOCOL_VERSION = '2024-11-05'
|
|
12
|
+
|
|
13
|
+
const dataDir = path.resolve(
|
|
14
|
+
process.env.MIO_DATA_DIR || path.join(process.cwd(), '.mio-intelligence')
|
|
15
|
+
)
|
|
16
|
+
const memoryPath = path.join(dataDir, 'memory.jsonl')
|
|
17
|
+
const tracePath = path.join(dataDir, 'traces.jsonl')
|
|
18
|
+
const experienceReusePath = path.join(dataDir, 'experience_reuse.jsonl')
|
|
19
|
+
fs.mkdirSync(dataDir, { recursive: true })
|
|
20
|
+
|
|
21
|
+
let runtimeContext = {}
|
|
22
|
+
try {
|
|
23
|
+
const rawContext = process.env.MIO_CONTEXT
|
|
24
|
+
if (rawContext) {
|
|
25
|
+
const parsed = JSON.parse(rawContext)
|
|
26
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
27
|
+
runtimeContext = parsed
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch (_) {
|
|
31
|
+
runtimeContext = {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function runtimeAgentId() {
|
|
35
|
+
return runtimeContext.agentId || null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createId(prefix) {
|
|
39
|
+
return `${prefix}_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function appendJsonl(file, value) {
|
|
43
|
+
fs.appendFileSync(file, `${JSON.stringify(value)}\n`, 'utf8')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readJsonl(file) {
|
|
47
|
+
if (!fs.existsSync(file)) return []
|
|
48
|
+
return fs
|
|
49
|
+
.readFileSync(file, 'utf8')
|
|
50
|
+
.split(/\r?\n/)
|
|
51
|
+
.filter(Boolean)
|
|
52
|
+
.map((line) => {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(line)
|
|
55
|
+
} catch (_) {
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.filter(Boolean)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let lastProjectCwd = null
|
|
63
|
+
let lastProjectName = null
|
|
64
|
+
|
|
65
|
+
function projectName() {
|
|
66
|
+
const currentCwd = process.cwd()
|
|
67
|
+
if (lastProjectCwd === currentCwd && lastProjectName) {
|
|
68
|
+
return lastProjectName
|
|
69
|
+
}
|
|
70
|
+
lastProjectCwd = currentCwd
|
|
71
|
+
if (runtimeContext.project) {
|
|
72
|
+
lastProjectName = runtimeContext.project
|
|
73
|
+
return lastProjectName
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
const commonDir = execFileSync(
|
|
77
|
+
'git',
|
|
78
|
+
['rev-parse', '--path-format=absolute', '--git-common-dir'],
|
|
79
|
+
{
|
|
80
|
+
cwd: currentCwd,
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
83
|
+
}
|
|
84
|
+
).trim()
|
|
85
|
+
if (commonDir) {
|
|
86
|
+
const repoRoot = path.dirname(commonDir)
|
|
87
|
+
const name = path.basename(repoRoot)
|
|
88
|
+
if (name) {
|
|
89
|
+
lastProjectName = name
|
|
90
|
+
return lastProjectName
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
} catch (_) {
|
|
94
|
+
// fall through to cwd basename
|
|
95
|
+
}
|
|
96
|
+
lastProjectName = path.basename(currentCwd)
|
|
97
|
+
return lastProjectName
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeTags(tags) {
|
|
101
|
+
if (Array.isArray(tags)) return tags.map((tag) => String(tag).trim()).filter(Boolean)
|
|
102
|
+
if (typeof tags === 'string') {
|
|
103
|
+
return tags
|
|
104
|
+
.split(',')
|
|
105
|
+
.map((tag) => tag.trim())
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
}
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function tokenize(text) {
|
|
112
|
+
const normalized = String(text || '').toLowerCase()
|
|
113
|
+
const latin = normalized.match(/[a-z0-9]+/g) || []
|
|
114
|
+
const cjk = (normalized.match(/[\u4e00-\u9fff]/g) || []).map((char) => `cjk:${char}`)
|
|
115
|
+
return [...latin, ...cjk]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function scoreRecord(record, queryTokens, project) {
|
|
119
|
+
if (project && record.project && record.project !== project) return 0
|
|
120
|
+
const haystack = tokenize(
|
|
121
|
+
`${record.content || ''} ${(record.tags || []).join(' ')} ${record.kind || ''}`
|
|
122
|
+
)
|
|
123
|
+
let score = 0
|
|
124
|
+
for (const token of queryTokens) {
|
|
125
|
+
if (haystack.includes(token)) score += 1
|
|
126
|
+
}
|
|
127
|
+
if (record.timestamp) {
|
|
128
|
+
const ageDays = Math.max(0, (Date.now() - Date.parse(record.timestamp)) / 86400000)
|
|
129
|
+
score += Math.max(0, 0.5 - ageDays * 0.05)
|
|
130
|
+
}
|
|
131
|
+
return score
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function queryMemory(args = {}) {
|
|
135
|
+
const query = String(args.query || '')
|
|
136
|
+
const project = args.project || projectName()
|
|
137
|
+
const limit = Math.min(Math.max(Number(args.limit) || 5, 1), 20)
|
|
138
|
+
const tokens = tokenize(query)
|
|
139
|
+
const results = readJsonl(memoryPath)
|
|
140
|
+
.filter((record) => scoreRecord(record, tokens, project) > 0)
|
|
141
|
+
.sort((a, b) => {
|
|
142
|
+
return scoreRecord(b, tokens, project) - scoreRecord(a, tokens, project)
|
|
143
|
+
})
|
|
144
|
+
.slice(0, limit)
|
|
145
|
+
return {
|
|
146
|
+
query,
|
|
147
|
+
project,
|
|
148
|
+
count: results.length,
|
|
149
|
+
results,
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function recordMemory(args = {}) {
|
|
154
|
+
const content = String(args.content || '').trim()
|
|
155
|
+
if (!content) throw new Error('memory.record requires a non-empty content')
|
|
156
|
+
const record = {
|
|
157
|
+
id: createId('mem'),
|
|
158
|
+
timestamp: new Date().toISOString(),
|
|
159
|
+
kind: args.kind || 'note',
|
|
160
|
+
content,
|
|
161
|
+
tags: normalizeTags(args.tags),
|
|
162
|
+
project: args.project || projectName(),
|
|
163
|
+
source: args.source || runtimeAgentId() || 'mcp',
|
|
164
|
+
}
|
|
165
|
+
appendJsonl(memoryPath, record)
|
|
166
|
+
return record
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function ingestObservation(args = {}) {
|
|
170
|
+
const traceId = String(args.trace_id || '').trim()
|
|
171
|
+
const eventType = String(args.event_type || '').trim()
|
|
172
|
+
if (!traceId) throw new Error('observer.ingest requires trace_id')
|
|
173
|
+
if (!eventType) throw new Error('observer.ingest requires event_type')
|
|
174
|
+
const event = {
|
|
175
|
+
id: createId('trace'),
|
|
176
|
+
timestamp: new Date().toISOString(),
|
|
177
|
+
trace_id: traceId,
|
|
178
|
+
event_type: eventType,
|
|
179
|
+
outcome: args.outcome || null,
|
|
180
|
+
payload: args.payload || {},
|
|
181
|
+
agent: args.agent || runtimeAgentId(),
|
|
182
|
+
host: args.host || 'mcp',
|
|
183
|
+
project: args.project || projectName(),
|
|
184
|
+
}
|
|
185
|
+
appendJsonl(tracePath, event)
|
|
186
|
+
return { recorded: true, event }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function policyCheck(args = {}) {
|
|
190
|
+
const action = String(args.action || '').trim()
|
|
191
|
+
if (!action) throw new Error('policy.check requires action')
|
|
192
|
+
const project = args.project || projectName()
|
|
193
|
+
const actionTokens = tokenize(action)
|
|
194
|
+
const traces = readJsonl(tracePath).filter((event) => {
|
|
195
|
+
if (project && event.project && event.project !== project) return false
|
|
196
|
+
const haystack = tokenize(
|
|
197
|
+
`${event.event_type || ''} ${JSON.stringify(event.payload || {})}`
|
|
198
|
+
)
|
|
199
|
+
return actionTokens.some((token) => haystack.includes(token))
|
|
200
|
+
})
|
|
201
|
+
const total = traces.length
|
|
202
|
+
const failures = traces.filter((event) =>
|
|
203
|
+
['failure', 'error', 'aborted', 'retry'].includes(String(event.outcome || '').toLowerCase())
|
|
204
|
+
).length
|
|
205
|
+
const risk = total > 0 ? failures / total : null
|
|
206
|
+
let suggestion
|
|
207
|
+
if (total === 0) {
|
|
208
|
+
suggestion = 'No history for this action. Treat as normal risk and record the outcome.'
|
|
209
|
+
} else if (risk >= 0.4) {
|
|
210
|
+
suggestion = 'Historically risky. Prefer an alternative approach or add rollback/verification.'
|
|
211
|
+
} else if (risk >= 0.2) {
|
|
212
|
+
suggestion = 'Moderate risk. Add verification before proceeding.'
|
|
213
|
+
} else {
|
|
214
|
+
suggestion = 'Low historical risk. Proceed with normal checks.'
|
|
215
|
+
}
|
|
216
|
+
const relatedMemories = readJsonl(memoryPath)
|
|
217
|
+
.filter((record) => scoreRecord(record, actionTokens, project) > 0)
|
|
218
|
+
.sort((a, b) => scoreRecord(b, actionTokens, project) - scoreRecord(a, actionTokens, project))
|
|
219
|
+
.slice(0, 3)
|
|
220
|
+
.map((record) => ({
|
|
221
|
+
id: record.id,
|
|
222
|
+
timestamp: record.timestamp,
|
|
223
|
+
kind: record.kind,
|
|
224
|
+
content: record.content,
|
|
225
|
+
tags: record.tags,
|
|
226
|
+
}))
|
|
227
|
+
return {
|
|
228
|
+
action,
|
|
229
|
+
project,
|
|
230
|
+
total,
|
|
231
|
+
failures,
|
|
232
|
+
risk: risk === null ? null : Number(risk.toFixed(3)),
|
|
233
|
+
suggestion,
|
|
234
|
+
related_memories: relatedMemories,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function recordExperienceReuse(args = {}) {
|
|
239
|
+
const sourceAgent = String(args.sourceAgent || '').trim()
|
|
240
|
+
const targetAgent = String(args.targetAgent || '').trim()
|
|
241
|
+
const experienceId = String(args.experienceId || '').trim()
|
|
242
|
+
if (!sourceAgent) throw new Error('experience.reuse requires sourceAgent')
|
|
243
|
+
if (!targetAgent) throw new Error('experience.reuse requires targetAgent')
|
|
244
|
+
if (!experienceId) throw new Error('experience.reuse requires experienceId')
|
|
245
|
+
|
|
246
|
+
const evidence = {
|
|
247
|
+
id: createId('xfer'),
|
|
248
|
+
timestamp: new Date().toISOString(),
|
|
249
|
+
sourceAgent,
|
|
250
|
+
targetAgent,
|
|
251
|
+
experienceId,
|
|
252
|
+
reuse: args.reuse === true || args.reuse === 'true',
|
|
253
|
+
behaviorChanged: args.behaviorChanged === true || args.behaviorChanged === 'true',
|
|
254
|
+
outcomeImproved: args.outcomeImproved === true || args.outcomeImproved === 'true',
|
|
255
|
+
project: args.project || projectName(),
|
|
256
|
+
notes: args.notes || null,
|
|
257
|
+
}
|
|
258
|
+
appendJsonl(experienceReusePath, evidence)
|
|
259
|
+
return evidence
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const TOOLS = [
|
|
263
|
+
{
|
|
264
|
+
name: 'mio.memory.query',
|
|
265
|
+
description: 'Search local Mio project memory for context, decisions, and historical problems.',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
query: { type: 'string', description: 'Question or topic to recall.' },
|
|
270
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current directory name.' },
|
|
271
|
+
limit: { type: 'number', description: 'Maximum results, between 1 and 20. Defaults to 5.' },
|
|
272
|
+
},
|
|
273
|
+
required: ['query'],
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'mio.memory.record',
|
|
278
|
+
description: 'Record a decision, project context, or historical problem into local Mio memory.',
|
|
279
|
+
inputSchema: {
|
|
280
|
+
type: 'object',
|
|
281
|
+
properties: {
|
|
282
|
+
content: { type: 'string', description: 'Memory content.' },
|
|
283
|
+
kind: { type: 'string', description: 'Memory kind, such as decision, context, or problem.' },
|
|
284
|
+
tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags.' },
|
|
285
|
+
project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
|
|
286
|
+
source: { type: 'string', description: 'Recording source. Defaults to mcp.' },
|
|
287
|
+
},
|
|
288
|
+
required: ['content'],
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: 'mio.observer.ingest',
|
|
293
|
+
description: 'Ingest an Agent Trace event such as tool_call, error, retry, or task_outcome.',
|
|
294
|
+
inputSchema: {
|
|
295
|
+
type: 'object',
|
|
296
|
+
properties: {
|
|
297
|
+
trace_id: { type: 'string', description: 'Stable trace identifier.' },
|
|
298
|
+
event_type: { type: 'string', description: 'Event type, such as tool_call, error, retry, task_outcome.' },
|
|
299
|
+
payload: { type: 'object', description: 'Event payload.' },
|
|
300
|
+
outcome: { type: 'string', description: 'Optional outcome: success, failure, error, aborted, retry.' },
|
|
301
|
+
agent: { type: 'string', description: 'Agent identifier.' },
|
|
302
|
+
host: { type: 'string', description: 'Host name. Defaults to mcp.' },
|
|
303
|
+
project: { type: 'string', description: 'Project name. Defaults to current directory name.' },
|
|
304
|
+
},
|
|
305
|
+
required: ['trace_id', 'event_type'],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: 'mio.policy.check',
|
|
310
|
+
description: 'Check historical risk for an action before executing it.',
|
|
311
|
+
inputSchema: {
|
|
312
|
+
type: 'object',
|
|
313
|
+
properties: {
|
|
314
|
+
action: { type: 'string', description: 'The action or tool operation to check.' },
|
|
315
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current directory name.' },
|
|
316
|
+
context: { type: 'string', description: 'Optional additional context.' },
|
|
317
|
+
},
|
|
318
|
+
required: ['action'],
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
name: 'mio.experience.reuse',
|
|
323
|
+
description: 'Record Phase 0 evidence of cross-agent experience reuse.',
|
|
324
|
+
inputSchema: {
|
|
325
|
+
type: 'object',
|
|
326
|
+
properties: {
|
|
327
|
+
sourceAgent: { type: 'string', description: 'Agent that produced the experience.' },
|
|
328
|
+
targetAgent: { type: 'string', description: 'Agent that reused the experience.' },
|
|
329
|
+
experienceId: { type: 'string', description: 'Stable experience or memory id.' },
|
|
330
|
+
reuse: { type: 'boolean', description: 'True when memory.query returned historical experience and it was reused.' },
|
|
331
|
+
behaviorChanged: { type: 'boolean', description: 'True when target Agent behavior changed because of the reused experience.' },
|
|
332
|
+
outcomeImproved: { type: 'boolean', description: 'True when the task outcome improved.' },
|
|
333
|
+
project: { type: 'string', description: 'Project name filter. Defaults to current repository.' },
|
|
334
|
+
notes: { type: 'string', description: 'Optional evidence notes.' },
|
|
335
|
+
},
|
|
336
|
+
required: ['sourceAgent', 'targetAgent', 'experienceId', 'reuse', 'behaviorChanged', 'outcomeImproved'],
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
async function callTool(name, args = {}) {
|
|
342
|
+
switch (name) {
|
|
343
|
+
case 'mio.memory.query':
|
|
344
|
+
return queryMemory(args)
|
|
345
|
+
case 'mio.memory.record':
|
|
346
|
+
return recordMemory(args)
|
|
347
|
+
case 'mio.observer.ingest':
|
|
348
|
+
return ingestObservation(args)
|
|
349
|
+
case 'mio.policy.check':
|
|
350
|
+
return policyCheck(args)
|
|
351
|
+
case 'mio.experience.reuse':
|
|
352
|
+
return recordExperienceReuse(args)
|
|
353
|
+
default:
|
|
354
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function jsonRpcResult(id, result) {
|
|
359
|
+
return { jsonrpc: '2.0', id, result }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function jsonRpcError(id, code, message, data) {
|
|
363
|
+
const error = { code, message }
|
|
364
|
+
if (data !== undefined) error.data = data
|
|
365
|
+
return { jsonrpc: '2.0', id, error }
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function send(message) {
|
|
369
|
+
if (message) process.stdout.write(`${JSON.stringify(message)}\n`)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function handleMessage(message) {
|
|
373
|
+
if (!message || typeof message !== 'object' || message.jsonrpc !== '2.0') return
|
|
374
|
+
if (message.method && message.method.startsWith('notifications/')) return
|
|
375
|
+
if (message.id === undefined) return
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
if (message.method === 'initialize') {
|
|
379
|
+
send(
|
|
380
|
+
jsonRpcResult(message.id, {
|
|
381
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
382
|
+
capabilities: { tools: {} },
|
|
383
|
+
serverInfo: SERVER_INFO,
|
|
384
|
+
})
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
}
|
|
388
|
+
if (message.method === 'ping') {
|
|
389
|
+
send(jsonRpcResult(message.id, {}))
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
if (message.method === 'tools/list') {
|
|
393
|
+
send(jsonRpcResult(message.id, { tools: TOOLS }))
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
if (message.method === 'tools/call') {
|
|
397
|
+
const name = message.params && message.params.name
|
|
398
|
+
const args = (message.params && message.params.arguments) || {}
|
|
399
|
+
const result = await callTool(name, args)
|
|
400
|
+
send(
|
|
401
|
+
jsonRpcResult(message.id, {
|
|
402
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
403
|
+
isError: false,
|
|
404
|
+
})
|
|
405
|
+
)
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
send(jsonRpcError(message.id, -32601, `Method not found: ${message.method}`))
|
|
409
|
+
} catch (error) {
|
|
410
|
+
send(jsonRpcError(message.id, -32603, error.message || String(error)))
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity })
|
|
415
|
+
|
|
416
|
+
rl.on('line', (line) => {
|
|
417
|
+
let message
|
|
418
|
+
try {
|
|
419
|
+
message = JSON.parse(line)
|
|
420
|
+
} catch (_) {
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
handleMessage(message).catch((error) => {
|
|
424
|
+
send(jsonRpcError(message && message.id, -32603, error.message || String(error)))
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
rl.on('close', () => {
|
|
429
|
+
process.exit(0)
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
module.exports = { TOOLS, callTool, handleMessage, readJsonl, appendJsonl }
|