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 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 }
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ function isInstalled() {
4
+ return false
5
+ }
6
+
7
+ function install() {
8
+ return {
9
+ changed: false,
10
+ message: 'OpenCode adapter is not implemented yet.',
11
+ }
12
+ }
13
+
14
+ module.exports = { isInstalled, install }
@@ -0,0 +1,14 @@
1
+ 'use strict'
2
+
3
+ function isInstalled() {
4
+ return false
5
+ }
6
+
7
+ function install() {
8
+ return {
9
+ changed: false,
10
+ message: 'WorkBuddy adapter is not implemented yet.',
11
+ }
12
+ }
13
+
14
+ module.exports = { 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 }